%matplotlib inline
from TriangleFunctions import *
During a lecture in a summer calculus class, I took to drawing random geometric patterns around the edges of my notes. As I was toying with different shapes and ideas for simple space filling curves, I came across an algorithm that happened to result in consistent patterns when drawn within triangles. Consider this simple algorithm below:
current_point
.current_point
, draw to the farthest edge of P, such that the line drawn is perpindicular to the chosen edge.current_point
equal to the end of this drawn perpindicular line, and repeat this recursive process for some number of steps.Following this "right angle algorithm" in detail on paper, I noticed a couple curious properties. The most prominent property I noticed was that given any "initial triangle" and any starting point, the algorithm would quickly converge to some repeating shape, which I will call the "ultimate shape".
Interested in finding the exact properties of this ultimate shape and putting my drawing skills to the test, I went home and wrote a program to generate a triangle and implement this right angle algorithm on it, graphing the result. This write up is a description of what I found when testing this algorithm, highlighting its more interesting features.
If you would like to see the code I wrote for this, visit the GitHub repository linked here.
The algorithm starts drawing from some point along the edge of the initial triangle, and draws a line to the farthest perpinducular edge, repeating the process from its new location in a recursive manner. When the algorithm is faced with two equal distances, it randomely picks one of the two edges to draw to. With this basic understanding of the algorithms' internals, we are equipt to begin the exploration into why it produces the ultimate shapes it does.
Picking an equilateral triangle to begin, I started the algorithm at one of its vertices and let it run for 100 steps, resulting in the graph shown below.
NOTE: The function return_points
takes two angles and the right point of the triangle to build, returning the necessary points to make the triangle with the passed exact angles and right end point. The function drawTriangle
takes (number_of_iterations, starting_point, outer_triangle_points, steps_to_show)
where number_of_iterations
is the number of steps the algorithm will execute, starting_point
is where the algorithm will being drawing, outer_triangle_points
is the set of three points generated by return_points
to define the outer triangle, and finally steps_to_show
is the index-range of steps to graph.
eq_triangle = return_points(60, 60, [4, 0])
visited_points = drawTriangle(100, [4, 0], eq_triangle, [0, 100])
As you can see, after just a few steps the algorithm converged to a different triangle within the outer one. It can be seen that the ultimate triangle has each edge perpindicular to one of the outer triangle's edges, with all three of its vertices touching the outer triangle's edges.
One way I like to think of the outer triangle is an initial condition to this algorithm that will "mold" the lines into their "natural resting points" over time. You can see the result of this above. It seems that with every new step, the outer triangle is forcing the algorithm into its final repeating shape.
To eak some more information out of this pattern, I wrote a function, give_info
to return the inner angles of this ultimate triangle. Below those inner angles are printed along with the last steps the algorithm took for more clarity.
visited_points = drawTriangle(100, [4, 0], eq_triangle, [90, 100])
give_info(visited_points, eq_triangle)
Outer Triangle Information: Inner angles: [60, 60, 60] Inner Triangle Information: Inner angles: [60, 60, 60]
Right off the bat, we notice that not only does this algorithm converge to triangle, but this ultimate triangle is similar to the outer one, as all the interior angles are the same! This is a rather interesting result.
As this initial equilateral outer triangles is scaled up and down, the convergence of the algorithm is not affected, however notice what happens when we change the starting point of the algorithm from (4,0) to (3,0) and (1,0), where the final steps the algorithm took for each situation are graphed below.
visited_points = drawTriangle(100, [3, 0], eq_triangle, [90, 100])
visited_points = drawTriangle(100, [1, 0], eq_triangle, [90, 100])
As the starting point of the algorithm shifted, the ultimate triangle flipped around the axis defined by x=2. Infact, when the starting point is to the left or right of 2, the ultimate triangle will always be pointing in one direction or the other; unless the algorithm starts at the edge points of (0,0) and (4,0), where the ultimate triangles orientation will be determined randomely. When the starting point is at 2, the algorithm picks a random side to follow due to the symmetry of the triangle, and the ultimate triangle will be oriented to either the left or right.
Curiousely enough, regardless of where the algorithm starts drawing from, the inner ultimate triangle is always the same size and has the same interior angles.
What if we applied this algorithm to an isosceles triangle? Take the example shown below, with the interior angles of both the ultimate inner triangle and initial triangle printed with the resulting graph.
iso1 = return_points(50, 65, [4, 0])
visited_points = drawTriangle(100, [3.6, 0], iso1, [0, 100])
give_info(visited_points, iso1)
Outer Triangle Information: Inner angles: [65, 65, 50] Inner Triangle Information: Inner angles: [50, 65, 65]
As you can see, the same thing happened for this particular isosceles triangle; the ultimate triangle the algorithm converged to is similar to the outer triangle!
At this point, you are likely thinking that the ultimate shape may not always be a triangle, depending on the properties of the initial (outer) triangle! I will get to that, but first I am going to show that this property of convergence to similar triangles also holds for scalene triangles.
scalene1 = return_points(60, 70, [4, 0])
visited_points = drawTriangle(100, [3.6, 0], scalene1, [0, 100])
give_info(visited_points, scalene1)
Outer Triangle Information: Inner angles: [50, 70, 60] Inner Triangle Information: Inner angles: [50, 60, 70]
Once again, we can see the inner ultimate triangle is similar to the outer triangle. One natural question may be, "If the computational resources are not available to find this ultimate triangle, how else might it be found exactly"?
The answer would be that this exact inner triangle could be found with relative ease using geometry. Take a look at the general diagram below for one of these cases.
We know a,b,c, and A,B,C. It is also known that each of the edges of the inner triangle will be perpindicular with its corresponding edge on the outer triangle, and that the inner ultimate triangle will be similar to the outer one (same interior angles). With this information, all the inner angles can be found, giving enough information to solve for x,y, and z shown in the diagram above. After finding these values, the points of the inner ultimate triangle can be found.
One interesting formula that can be used to approximate the side lengths of the inner ultimate triangle is given by
a′≈sin(γ)⋅aφwhere γ is the peak angle of the initial triangle, a′ is the inner side length to be approximated, a is the corresponding outer side length, and φ=1.61803⋯, the Golden Ratio. The resulting error of the approximations when using this formula is highly dependent on the triangle being analyzed. In the example triangles I worked on, I saw side length approximations errors of the ultimate inner triangle between 0.1% and 7%.
The side length to be solved for, a′, can be found exactly with
a′=sin(γ)⋅acwhere c≈φ. I have a hunch c is given by a function of the peak angle γ, however I have yet to find this function exactly, assuming it exists.
As alluded to earlier, when the angles of the outer triangle are adjusted, the algorithm can generate other interesting repeating shapes. One of the simpler repeating shapes is given below from an isosceles triangle. As a quick note, for most of the examples below I chose not to show the first steps the algorithm took as seeing them is usually not especially informative and they clutter the graph.
iso2 = return_points(30, 75, [4, 0])
visited_points = drawTriangle(100, [3.6, 0], iso2, [90, 100])
This is the first repeating ultimate shape we have seen that produces something other than a triangle. The above ultimate shape is nothing to get too excited about however, so lets start by slightly increasing and decreasing the interior angles of the isosceles triangle to see what else the algorithm comes up with.
iso3 = return_points(40, 70, [4, 0])
visited_points = drawTriangle(100, [1, 0], iso3, [90, 100])
iso4 = return_points(20, 80, [4, 0])
visited_points = drawTriangle(100, [3, 0], iso4, [80, 100])
Those patterns are much more interesting! Recall that the algorithm will always draw the longest possible line it can while still obeying the right angle rule, as we can see the result of this taking form in the previous two examples. For this specific isosceles triangle, we can see that as the top angle decreases, the number of "zig-zags" formed within the ultimate shape increases, as the angles are much steeper.
As shown below, scalene triangles succumb to a similar phenomena as well, producing different repeating shapes.
scalene2 = return_points(40, 80, [4, 0])
visited_points = drawTriangle(110, [3, 0], scalene2, [90, 110])
scalene3 = return_points(20, 86, [4, 0])
visited_points = drawTriangle(110, [2, 0], scalene3, [80, 110])
Lets take a look at what happens when we move the top angle in the other direction, starting with a 45−90−45 triangle. In this case, it is interesting seeing all the steps the algorithm takes, so I plot them all below. The starting point is (3.7,0).
iso6 = return_points(90, 45, [4, 0])
visited_points = drawTriangle(130, [3.7, 0], iso6, [0, 130])
The final ultimate shape and its properties are shown below.
visited_points = drawTriangle(110, [3, 0], iso6, [100, 110])
give_info(visited_points, iso6)
Outer Triangle Information: Inner angles: [45, 45, 90] Inner angles of each triangle are: [45, 90, 45]
When the algorithm was implemented on the above right triangle, it produced some interesting results. The most immidiately noticeable of which being that the hourglass like ultimate shape, is built out of two 45−90−45 triangles!
In the current implementation of the algorithm I have prevented it from drawing along edges or meeting the triangles vertices, to force it into developing the hourglass ultimate shape seen above. It is for this reason that on what looks to be the fourth step above (shown on the first graph of the triangle), the algorithm chose to draw down instead of up to the top vertice of the triangle. If the algorithm had been allowed to do so, the resulting inner shape would have been the two ultimate triangles flipped around the y-axis and scaled up, as pictured below by a previous implementation of the algorithm.
The algorithm is applied to a few more obtuse triangles to demonstrate this property of the algorithm converging to an "hour-glass" ultimate shape, composed to two similar triangles.
iso7 = return_points(100, 40, [4, 0])
visited_points = drawTriangle(150, [3.4, 0], iso7, [100, 150])
give_info(visited_points, iso7)
Outer Triangle Information: Inner angles: [40, 40, 100] Inner angles of each triangle are: [40, 100, 40]
scalene4 = return_points(110, 30, [4, 0])
visited_points = drawTriangle(150, [1, 0], scalene4, [100, 150])
iso8 = return_points(140, 20, [4, 0])
visited_points = drawTriangle(150, [2.2, 0], iso8, [100, 150])
While this property is true for all obtuse triangles, note what happens as γ, the peak angle, approaches 180 degrees. As γ→180, the range of starting points from which the algorithm will converge to this ultimate hour-glass shape decreases, approaching the center of the triangle.
Notice what happens if the algorithm starts outside of this "critical range".
iso9 = return_points(140, 20, [4, 0])
visited_points = drawTriangle(100, [2.3, 0], iso9, [0, 100])
The algorithm gets "stuck" in a corner and approaches the left/right endpoints as the number of steps approaches infinity!
We have noticed that the ultimate shape may or may not be a triangle in different initial triangles. An important question is "can we define a set of angles for each type of triangle, where the algorithm will always converge to an ultimate triangle?".
Lets begin with finding rules for when an isosceles triangle will or will not result in an ultimate triangle. Consider the two initial isosceles triangles below. Notice the minute difference in their interior angles, and the large difference in the ultimate shapes.
iso2 = return_points(43.2, 68.4, [4, 0])
visited_points = drawTriangle(100, [3.6, 0], iso2, [90, 100])
iso2 = return_points(43.3, 68.35, [4, 0])
visited_points = drawTriangle(100, [4, 0], iso2, [90, 100])
iso2 = return_points(43.3, 68.35, [4, 0])
visited_points = drawTriangle(100, [4, 0], iso2, [0, 10])
As we can see in the third graph, the algorithm (starting at (4,0)) seemed to be converging to a triangle, until on the 6th step it went up again instead of down, as drawing up again was just longer than drawing down. What this means is that we have found a "critical point" for lack of a better term. We already know of the first critical point for isosceles triangles; it is the 45−90−45 triangle, where when the angles are at that point the ultimate shape will not be a triangle. The graphs above show us that the inflection point on the other end is when the interior angles are about 68.35−43.3−68.35. Now that we know the two critical points, we can begin to define a range on which the initial triangles will have triangular ultimate shapes.
We will define each of the "foot" angles in an isosceles triangle as x, and the "peak" angle as y. From this we deduce the following rule for the interior angles of isosceles triangles 2x+y=180
Visually, when the value of x is shifted along this range of convergence towards x=60, the outer and ultimate triangle both approach the equilateral triangle, filling into it at x=60.
A similar process can be used to find the subset of angles that define a scalene triangle with an ultimate shape equal to a triangle. For example, if we take the line perpindicular to y=−2x+180 that passes through (60,60) we get y=12x+30. The points along this line give interior angles that can be used to construct scalene triangles, except when x=60.
Now, a pair of critical points can be found for scalene triangles, and the range of points between the two critical points along the line defined by y=12x+30 will define scalene triangles with triangular ultimate shapes!
We now have an easy method for determining if a given initial triangle will have a triangular ultimate shape or not!
Given these findings, we are in a position to make some simple conjectures and explore other questions.
For any given triangle P, there may exists a single similar triangle P′, such that when P′ is placed within P, P′ can touch all three of P's edges, and will form a right angle with each of P's edges. Such a triangle P′ does not exist for triangles with an angle greater than or equal to 90 degrees, and does not exist for specific accute triangles. A P′ does exist however for every equilateral triangle.
In my belief, the best or perhaps only way of testing this conjecture is by using this "right angle algorithm" on a triangle P to see if such a triangle P′ can be found.
This project was certainly a lot of fun, however I have many more ideas I would like to explore and questions I would like to have answered. I list some of these below.
Questions & Exploration: