{"id":2041,"date":"2025-06-05T07:00:01","date_gmt":"2025-06-05T12:00:01","guid":{"rendered":"https:\/\/wollen.org\/blog\/?p=2041"},"modified":"2025-05-31T21:36:16","modified_gmt":"2025-06-01T02:36:16","slug":"watch-it-fly-by-as-the-pendulum-swings","status":"publish","type":"post","link":"https:\/\/wollen.org\/blog\/2025\/06\/watch-it-fly-by-as-the-pendulum-swings\/","title":{"rendered":"Watch it fly by as the pendulum swings"},"content":{"rendered":"<p>First things first: Yes, that&#8217;s a <em>Linkin Park<\/em> lyric. I&#8217;m not proud of it but there aren&#8217;t enough pop-culture pendulum references.<\/p>\n<p>I think most people understand what a pendulum is. It&#8217;s when you have a heavy thing that swings on a cable or rod, like an old grandfather clock. In this post we&#8217;ll assume it&#8217;s an &#8220;idealized&#8221; pendulum. That means the rod is massless, the heavy thing is a point mass, and there&#8217;s no friction or air resistance. It almost seems like cheating but it makes the math a lot easier.<\/p>\n<p>Let&#8217;s skip to the relevant equations.<\/p>\n<hr \/>\n<h4>1. Theory.<\/h4>\n<p>The angle theta (\u03b8) that a pendulum makes with the vertical axis, as a function of time, can be expressed like this:<\/p>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/06\/pendulum_eq1-1.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-2047 size-full\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/06\/pendulum_eq1-1.png\" alt=\"\" width=\"380\" height=\"91\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/06\/pendulum_eq1-1.png 380w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/06\/pendulum_eq1-1-300x72.png 300w\" sizes=\"auto, (max-width: 380px) 100vw, 380px\" \/><\/a><\/p>\n<p>Where&#8230;<\/p>\n<ul>\n<li><strong>\u03b8<sub>0<\/sub><\/strong> is the initial angle (radians)<\/li>\n<li><strong>g<\/strong> is the &#8220;little g&#8221; gravitational constant (9.81 m\/s<sup>2<\/sup>)<\/li>\n<li><strong>L<\/strong> is the rod length (meters)<\/li>\n<li><strong>t<\/strong> is time (seconds)<\/li>\n<\/ul>\n<p>The catch is that this formulation uses the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Small-angle_approximation\" target=\"_blank\" rel=\"noopener\">small angle approximation<\/a>, which is indeed an approximation. And it only holds true up to 0.5 radians or so. If we pulled the pendulum back further and released it, our model would begin to diverge from reality.<\/p>\n<p>Here&#8217;s a GIF to illustrate exactly what theta is measuring:<\/p>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/06\/pendulum_units-1.gif\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-2256 size-full\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/06\/pendulum_units-1.gif\" alt=\"\" width=\"700\" height=\"650\" \/><\/a><\/p>\n<p>Theta is the angle formed by the pendulum rod and the vertical axis. Usually we measure angles with respect to a horizontal axis, so keep that in mind for later.<\/p>\n<hr \/>\n<h4>2. Code.<\/h4>\n<p>The plan is to draw two animated plots side-by-side. On the left will be a pendulum swinging back and forth like the GIF above. On the right will be theta as a function of time. As we saw from the formula, it&#8217;s a cosine function, so we&#8217;ll be plotting a waveform.<\/p>\n<p>Most of the code will be nested inside a large <code>for<\/code> loop. We&#8217;ll create one frame of the animation during each iteration of the loop. Once all the images are generated, we can open them in image editing software\u2014I&#8217;ll use <a href=\"https:\/\/www.gimp.org\/\" target=\"_blank\" rel=\"noopener\">GIMP<\/a>\u2014and save them as an animated GIF.<\/p>\n<p>Start by declaring the constants. We&#8217;ll use a rod length of 2 meters to keep things interesting. Make the initial angle (<code>THETA_INITIAL<\/code>) 0.5 radians to satisfy the small angle approximation. The gravitational constant is always 9.81 m\/s<sup>2<\/sup>. <code>T_STEP<\/code> is how much time (<code>t<\/code>) will elapse between frames. The smaller the number, the smoother the animation, but file size increases as well. A value of 0.02 implies 50 frames per second, which looks good to me.<\/p>\n<p>284 frames might seem like a random number but it&#8217;s not. A pendulum&#8217;s period, i.e. one full cycle from left to right and back again, is defined like this:<\/p>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/06\/pendulum_eq2.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-2050\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/06\/pendulum_eq2.png\" alt=\"\" width=\"314\" height=\"114\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/06\/pendulum_eq2.png 314w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/06\/pendulum_eq2-300x109.png 300w\" sizes=\"auto, (max-width: 314px) 100vw, 314px\" \/><\/a><\/p>\n<p>If you do the math, one cycle takes about 2.84 seconds. I think it will be good to animate two complete cycles, which at 0.02 seconds per frame works out to 284 frames.<\/p>\n<p><code>t<\/code> is our time variable. Naturally it will begin at 0.0 seconds.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">from math import sqrt, sin, cos\r\nfrom numpy import arange\r\nimport matplotlib.pyplot as plt\r\n\r\n\r\nLENGTH = 2\r\nTHETA_INITIAL = 0.5\r\nG_CONSTANT = 9.81\r\nT_STEP = 0.02\r\nNUMBER_OF_FRAMES = 284\r\n\r\nplt.style.use(\"wollen_dark.mplstyle\")\r\n\r\nt = 0.0\r\nt_list = []\r\ntheta_list = []\r\n\r\nfor frame_number in range(NUMBER_OF_FRAMES):\r\n\r\n    print(f\"{frame_number + 1}\/{NUMBER_OF_FRAMES}\")\r\n\r\n    [...]<\/pre>\n<p>Now we can begin plotting inside our loop. Create a figure with two Axes in a horizontal layout\u20141 row, 2 columns.<\/p>\n<p>Implement the equation above to calculate theta as a function of time. For the plot on the right, <code>ax1<\/code>, I&#8217;d like to trace out theta&#8217;s curve piece by piece. To do that, append the current values of <code>theta<\/code> and <code>t<\/code> to their corresponding lists. This will save all of theta&#8217;s history up to the current point in time.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">for frame_number in range(NUMBER_OF_FRAMES):\r\n\r\n    [...]\r\n\r\n    fig, (ax0, ax1) = plt.subplots(1, 2)\r\n\r\n    theta = THETA_INITIAL * cos(sqrt(G_CONSTANT \/ LENGTH) * t)\r\n\r\n    t_list.append(t)\r\n\r\n    theta_list.append(theta)\r\n\r\n    [...]<\/pre>\n<p>With theta&#8217;s value in hand, let&#8217;s jump back to <code>ax0<\/code> and plot the pendulum. It will hang from the origin (0, 0). We&#8217;ll do this in two parts: first the rod, then the mass.<\/p>\n<p>Since we know the rod&#8217;s angle and length, we can use basic trigonometry to calculate its position. Notice that the x-component of the rod uses <code>sin<\/code> and the y-component uses <code>cos<\/code>. It&#8217;s the reverse of how this usually works. That&#8217;s because theta forms an angle with the vertical axis rather than the horizontal one. It&#8217;s a little unusual but it&#8217;s perfectly fine.<\/p>\n<p>I&#8217;m hard-coding axis arguments for brevity&#8217;s sake. We could parameterize them based on <code>LENGTH<\/code> and <code>THETA_INITIAL<\/code> but the code would quickly become difficult to read.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">for frame_number in range(NUMBER_OF_FRAMES):\r\n\r\n    [...]\r\n\r\n    ax0.plot([0, 0 - LENGTH * sin(theta)],\r\n             [0, 0 - LENGTH * cos(theta)],\r\n             color=\"#988ED5\")\r\n\r\n    ax0.scatter([0 - LENGTH * sin(theta)],\r\n                [0 - LENGTH * cos(theta)],\r\n                color=\"#988ED5\",\r\n                s=400)\r\n\r\n    ax0.set(xticks=arange(-1.5, 2.0, 0.5),\r\n            xlim=(-1.6, 1.6),\r\n            yticks=arange(-2.5, 1.0, 0.5),\r\n            ylim=(-2.6, 0.6))\r\n\r\n    [...]<\/pre>\n<p>Now jump to the right-hand side and plot theta. Remember that we&#8217;re continuously updating a ledger of values so we just need to pass those lists to <code>plot<\/code>.<\/p>\n<p>I think it looks nice to call <code>scatter<\/code> and place a marker at the leading edge of the curve. Simply pass the most recent values of <code>t<\/code> and <code>theta<\/code>, repackaged as lists.<\/p>\n<p>The y-axis is in units of radians so it would be nice to set ticks in multiples of \u03c0. But we&#8217;re dealing with very small angles so I think it&#8217;s better to stick with basic floats.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">for frame_number in range(NUMBER_OF_FRAMES):\r\n\r\n    [...]\r\n\r\n    ax1.plot(t_list,\r\n             theta_list,\r\n             color=\"#B0E441\")\r\n\r\n    ax1.scatter([t_list[-1]],\r\n                [theta_list[-1]],\r\n                color=\"#B0E441\",\r\n                s=50)\r\n\r\n    ax1.set(xticks=arange(0.0, 7.0, 1.0),\r\n            xlim=(-0.2, 6.2),\r\n            yticks=arange(-0.5, 0.75, 0.25),\r\n            ylim=(-0.53, 0.53))\r\n\r\n    [...]<\/pre>\n<p>Set axis labels. We&#8217;re plotting a lot of different units so it&#8217;s good to be very explicit.<\/p>\n<p>Then save the figure. I&#8217;m dumping image files into a &#8220;frames&#8221; folder.<\/p>\n<p>Before exiting the loop, remember to increment the time variable, <code>t<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">for frame_number in range(NUMBER_OF_FRAMES):\r\n\r\n    [...]\r\n\r\n    ax0.set(xlabel=\"Distance (m)\",\r\n            ylabel=\"Distance (m)\",\r\n            title=\"Pendulum Position\")\r\n\r\n    ax1.set(xlabel=\"Time (s)\",\r\n            ylabel=\"Radians\",\r\n            title=\"Angle \u03b8\")\r\n\r\n    plt.savefig(f\"frames\/{frame_number}.png\")\r\n\r\n    t += T_STEP<\/pre>\n<p>After running the script, we&#8217;ll have 284 PNG images in a &#8220;frames&#8221; folder. I&#8217;ll open them in GIMP and export an animated GIF.<\/p>\n<p>Another option is to use <a href=\"https:\/\/www.ffmpeg.org\/\" target=\"_blank\" rel=\"noopener\">FFMPEG<\/a> and automate the GIF creation process. It&#8217;s a command line tool but there are Python packages to help you call it directly from a script.<\/p>\n<hr \/>\n<h4>3. The output.<\/h4>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/06\/pendulum_demo-1.gif\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-2064 size-full\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/06\/pendulum_demo-1.gif\" alt=\"\" width=\"1400\" height=\"700\" \/><\/a><\/p>\n<p>We initially released the pendulum from an angle of 0.5 radians. There&#8217;s no friction in our idealized system so it swings exactly that far in the other direction. Repeat ad infinitum.<!-- HFCM by 99 Robots - Snippet # 15: endmark-python -->\n<span class=\"endmark-python\"><\/span>\n<!-- \/end HFCM by 99 Robots -->\n<\/p>\n<hr \/>\n<p><a href=\"https:\/\/wollen.org\/misc\/pendulum_2025.zip\"><strong>Download the Matplotlib style.<\/strong><\/a><\/p>\n<p><strong>Full code:<\/strong><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">from math import sqrt, sin, cos\r\nfrom numpy import arange\r\nimport matplotlib.pyplot as plt\r\n\r\n\r\nLENGTH = 2\r\nTHETA_INITIAL = 0.5\r\nG_CONSTANT = 9.81\r\nT_STEP = 0.02\r\nNUMBER_OF_FRAMES = 284\r\n\r\nplt.style.use(\"wollen_dark.mplstyle\")\r\n\r\nt = 0.0\r\nt_list = []\r\ntheta_list = []\r\n\r\nfor frame_number in range(NUMBER_OF_FRAMES):\r\n\r\n    print(f\"{frame_number + 1}\/{NUMBER_OF_FRAMES}\")\r\n\r\n    fig, (ax0, ax1) = plt.subplots(1, 2)\r\n\r\n    theta = THETA_INITIAL * cos(sqrt(G_CONSTANT \/ LENGTH) * t)\r\n\r\n    t_list.append(t)\r\n\r\n    theta_list.append(theta)\r\n    \r\n    ax0.plot([0, 0 - LENGTH * sin(theta)],\r\n             [0, 0 - LENGTH * cos(theta)],\r\n             color=\"#988ED5\")\r\n\r\n    ax0.scatter([0 - LENGTH * sin(theta)],\r\n                [0 - LENGTH * cos(theta)],\r\n                color=\"#988ED5\",\r\n                s=400)\r\n\r\n    ax0.set(xticks=arange(-1.5, 2.0, 0.5),\r\n            xlim=(-1.6, 1.6),\r\n            yticks=arange(-2.5, 1.0, 0.5),\r\n            ylim=(-2.6, 0.6))\r\n\r\n    ax1.plot(t_list,\r\n             theta_list,\r\n             color=\"#B0E441\")\r\n\r\n    ax1.scatter([t_list[-1]],\r\n                [theta_list[-1]],\r\n                color=\"#B0E441\",\r\n                s=50)\r\n\r\n    ax1.set(xticks=arange(0.0, 7.0, 1.0),\r\n            xlim=(-0.2, 6.2),\r\n            yticks=arange(-0.5, 0.75, 0.25),\r\n            ylim=(-0.53, 0.53))\r\n\r\n    ax0.set(xlabel=\"Distance (m)\",\r\n            ylabel=\"Distance (m)\",\r\n            title=\"Pendulum Position\") \r\n\r\n    ax1.set(xlabel=\"Time (s)\",\r\n            ylabel=\"Radians\",\r\n            title=\"Angle \u03b8\")\r\n\r\n    plt.savefig(f\"frames\/{frame_number}.png\")\r\n\r\n    t += T_STEP<\/pre>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>First things first: Yes, that&#8217;s a Linkin Park lyric. I&#8217;m not proud of it but there aren&#8217;t enough pop-culture pendulum references. I think most people understand what a pendulum is. It&#8217;s when you have a heavy thing that swings on<\/p>\n","protected":false},"author":1,"featured_media":3165,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[40,561],"tags":[435,387,437,22,221,438,47,24,433,297,46,25,436,439,434],"class_list":["post-2041","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-math","category-animated","tag-angle","tag-animated","tag-animation","tag-data","tag-equation","tag-gif","tag-math","tag-matplotlib","tag-pendulum","tag-physics","tag-plot","tag-python","tag-radians","tag-small-angle","tag-theta"],"_links":{"self":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/2041","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/comments?post=2041"}],"version-history":[{"count":32,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/2041\/revisions"}],"predecessor-version":[{"id":3166,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/2041\/revisions\/3166"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/media\/3165"}],"wp:attachment":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/media?parent=2041"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/categories?post=2041"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/tags?post=2041"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}