{"id":1909,"date":"2025-02-06T07:00:11","date_gmt":"2025-02-06T13:00:11","guid":{"rendered":"https:\/\/wollen.org\/blog\/?p=1909"},"modified":"2025-01-28T16:19:25","modified_gmt":"2025-01-28T22:19:25","slug":"a-field-guide-to-two-dimensional-arrays","status":"publish","type":"post","link":"https:\/\/wollen.org\/blog\/2025\/02\/a-field-guide-to-two-dimensional-arrays\/","title":{"rendered":"A field guide to two-dimensional arrays"},"content":{"rendered":"<p>Usually on the blog we apply a function to each element of a one-dimensional array, like <code>[1, 2, 3]<\/code>, and get back another array, like <code>[10, 20, 30]<\/code>. Then we use both lists to plot a series of (x, y) ordered pairs.<\/p>\n<p>Today I&#8217;d like to expand on that process and apply functions to two-dimensional arrays. You can think of them like lists of lists. For example:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">[[1, 2, 3],\r\n [4, 5, 6],\r\n [7, 8, 9]]<\/pre>\n<p>We can streamline things by building on a <a href=\"https:\/\/wollen.org\/blog\/2024\/08\/how-to-simulate-a-universe-of-dots-on-a-screen\/\">previous blog post<\/a>. Back in August we modeled gravitational forces and plotted the motion of stars. Let&#8217;s revisit the universe we created then.<\/p>\n<p>Pictured is a different neighborhood within the universe but the same physics apply. Think of the yellow body as our sun and the blue body as a comet making a rare visit. The arrow represents how much and in what direction the comet is accelerated by the gravitational force applied to it.<\/p>\n<p>Remember, the magnitude of the force depends on the masses of the bodies and the square of the distance between them.<\/p>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/gravitational_motion_arrow.gif\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-1914\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/gravitational_motion_arrow.gif\" alt=\"\" width=\"800\" height=\"800\" \/><\/a><\/p>\n<p>The motion makes total sense if you&#8217;ve played Asteroids, the arcade game. Even when you stop applying a force (thrust) your ship keeps moving. It isn&#8217;t like driving a car here on earth where friction will eventually bring you to a stop.<\/p>\n<p>As our comet drifts away from the sun, the sun stops applying a force to it and it&#8217;s no longer accelerated, but it keeps drifting in the direction it was already going. This demonstrates Newton&#8217;s First Law: An object in motion will remain in motion until it&#8217;s acted on by an external force.<\/p>\n<figure id=\"attachment_1915\" aria-describedby=\"caption-attachment-1915\" style=\"width: 640px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/asteroids_1979.gif\"><img loading=\"lazy\" decoding=\"async\" class=\"wp-image-1915 size-full\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/asteroids_1979.gif\" alt=\"\" width=\"640\" height=\"480\" \/><\/a><figcaption id=\"caption-attachment-1915\" class=\"wp-caption-text\">Asteroids was an arcade game released by Atari in 1979.<\/figcaption><\/figure>\n<hr \/>\n<p>We&#8217;ve spent some time working with gravitational forces. Now let&#8217;s go a step further and plot a <em>gravitational field<\/em>. It might sound complicated but it&#8217;s not. It just means that when one mass approaches another, a force is applied to it. If a comet travels near the sun, it will enter the sun&#8217;s gravitational field and a force will be applied to the comet, causing its motion to change. This region of potential interaction is called a gravitational field.<\/p>\n<p>The important concept is that the force depends on where exactly the comet is located. At each point within the field there is a certain potential for interaction. Our goal is to define and visualize each of the points.<\/p>\n<p>We can visualize a field by drawing a bunch of arrows all over it. The arrows will point in whatever direction a body would experience a force at that point. And the bigger the arrow, the stronger the force.<\/p>\n<p>Fortunately, Matplotlib has us covered when it comes to drawing a bunch of arrows in a grid pattern. This is called a <em>quiver plot<\/em>.<\/p>\n<figure id=\"attachment_1918\" aria-describedby=\"caption-attachment-1918\" style=\"width: 200px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/arrow_quiver.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-medium wp-image-1918\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/arrow_quiver-200x300.png\" alt=\"\" width=\"200\" height=\"300\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/arrow_quiver-200x300.png 200w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/arrow_quiver.png 589w\" sizes=\"auto, (max-width: 200px) 100vw, 200px\" \/><\/a><figcaption id=\"caption-attachment-1918\" class=\"wp-caption-text\">A quiver is a container for holding arrows.<\/figcaption><\/figure>\n<hr \/>\n<h4>Prepare the data.<\/h4>\n<p>Let&#8217;s plot the &#8220;universe&#8221; pictured above with a yellow sun located at (20, 40). First define our constants, which are entirely arbitrary and not to scale.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">STAR_POS_X = 20\r\nSTAR_POS_Y = 40\r\nSTAR_MASS = 4000\r\nCOMET_MASS = 100\r\nG_CONSTANT = 1.0<\/pre>\n<p>We&#8217;re talking about quantities with magnitude and direction, which means we&#8217;re dealing with vectors. That means it&#8217;s time to import <a href=\"https:\/\/numpy.org\/\" target=\"_blank\" rel=\"noopener\">numpy<\/a>.<\/p>\n<p>This is where we begin working with two-dimensional arrays. Use <code>meshgrid<\/code> to create a sort of coordinate system for the universe.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import numpy as np\r\n\r\nx_grid, y_grid = np.meshgrid(range(-100, 105, 5),\r\n                             range(-100, 105, 5)\r\n                             )<\/pre>\n<p>The returned arrays look like this:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">x_grid = [[-100  -95  -90 ...   90   95  100]\r\n          [-100  -95  -90 ...   90   95  100]\r\n          [-100  -95  -90 ...   90   95  100]\r\n          ...\r\n          [-100  -95  -90 ...   90   95  100]\r\n          [-100  -95  -90 ...   90   95  100]\r\n          [-100  -95  -90 ...   90   95  100]]\r\n\r\n\r\ny_grid = [[-100 -100 -100 ... -100 -100 -100]\r\n          [ -95  -95  -95 ...  -95  -95  -95]\r\n          [ -90  -90  -90 ...  -90  -90  -90]\r\n          ...\r\n          [  90   90   90 ...   90   90   90]\r\n          [  95   95   95 ...   95   95   95]\r\n          [ 100  100  100 ...  100  100  100]]<\/pre>\n<p>You should consider the arrays bound together. Each corresponding pair of values essentially forms an ordered pair, (x, y). Together the arrays define a grid that spans our whole universe.<\/p>\n<p>These arrays are like the independent variable, x, on an old-fashioned line plot. If we were working with one-dimensional data, we would apply a function to the values of x and get back another series of values, y. In this case, we&#8217;ll apply a function to each point within the grid and get back a new two-dimensional array.<\/p>\n<p>As we learned in the <a href=\"https:\/\/wollen.org\/blog\/2024\/08\/how-to-simulate-a-universe-of-dots-on-a-screen\/\">previous post<\/a>, the equation for gravitational force between two bodies looks like this:<\/p>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/08\/eq_universal-gravitation.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-1376 size-full\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/08\/eq_universal-gravitation.png\" alt=\"\" width=\"355\" height=\"69\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/08\/eq_universal-gravitation.png 355w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2024\/08\/eq_universal-gravitation-300x58.png 300w\" sizes=\"auto, (max-width: 355px) 100vw, 355px\" \/><\/a><\/p>\n<p>&#8230; where <strong>m<sub>1<\/sub><\/strong> and <strong>m<sub>2<\/sub><\/strong> are the masses of the bodies, <strong>d<\/strong> is the distance between their centers, and <strong>G<\/strong> is the gravitational constant.<\/p>\n<p>But we&#8217;ll have to stretch our thinking to implement it for two-dimensional data.<\/p>\n<p>First let&#8217;s calculate the denominator, distance. Remember, we&#8217;re finding the sun&#8217;s distance from each point in the grid.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">distance_x = STAR_POS_X - x_grid\r\ndistance_y = STAR_POS_Y - y_grid\r\ndistance = np.sqrt(distance_x**2 + distance_y**2)<\/pre>\n<p>Notice how easy numpy makes it to &#8220;vectorize&#8221; functions across whole two-dimensional arrays. We don&#8217;t have to loop through each element and repeat the operation over and over again.<\/p>\n<p>Since the sun exists at a specific point within the grid, (20, 40), distance at that point is zero, and we can&#8217;t divide by zero. We can use <code>ma.masked_array<\/code> to filter out values from the array and avoid an error. Let&#8217;s add a fudge factor up to 15 so we&#8217;ll have room to plot the sun.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">distance = np.ma.masked_array(distance, distance &lt;= 15)<\/pre>\n<p>Now that we know distance, use the equation above to calculate the force&#8217;s magnitude.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">force = G_CONSTANT * STAR_MASS * COMET_MASS \/ distance**2<\/pre>\n<p>At this point, we don&#8217;t know the force&#8217;s direction so we couldn&#8217;t plot arrows. We could create a sort of &#8220;heat map&#8221; that uses color to indicate the gravitational force&#8217;s strength at each point. But I&#8217;ll save that for another post.<\/p>\n<p>Today we need to know direction so use your trigonometry skills to find the x- and y-components of each arrow. We&#8217;re only considering one mass in this universe so every arrow will point directly at (20, 40).<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">angle = np.atan2(distance_y, distance_x)\r\n\r\nx_components = force * np.cos(angle)\r\ny_components = force * np.sin(angle)<\/pre>\n<p>The operations here could definitely be simplified. My linear algebra isn&#8217;t the strongest so I&#8217;m okay with writing a few extra lines of code.<\/p>\n<hr \/>\n<h4>Plot the data.<\/h4>\n<p>I&#8217;ll use a custom Matplotlib style that I&#8217;ll link at the bottom of this post. Create a figure and Axes instance for plotting.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import matplotlib.pyplot as plt\r\n\r\nplt.style.use(\"grav_field.mplstyle\")\r\n\r\nfig, ax = plt.subplots()<\/pre>\n<p><code>quiver<\/code> has four required parameters: x-position, y-position, x-component, and y-component. It needs to know where arrows are located along with their magnitude and direction. We pass in the two-dimensional arrays that we just created. <code>pivot<\/code> specifies which part of the arrow should touch the point\u2014tip, tail, or middle. Tip works best for this application.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">ax.quiver(x_grid, y_grid, x_components, y_components, pivot=\"tip\")<\/pre>\n<p>Let&#8217;s also plot the sun as it appears in the GIF.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">ax.scatter([STAR_POS_X], [STAR_POS_Y], color=\"#F5E342\", s=STAR_MASS, edgecolor=\"black\", linewidth=0.5)<\/pre>\n<p>Finally, set ticks, window limits, and save the figure.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">ax.set(xticks=range(-100, 120, 20),\r\n       yticks=range(-100, 120, 20),\r\n       xlim=(-103, 103),\r\n       ylim=(-103, 103)\r\n       )\r\n\r\nplt.savefig(\"grav_quiver.png\")<\/pre>\n<hr \/>\n<h4>The output.<\/h4>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/grav_quiver.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-1924\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/grav_quiver.png\" alt=\"\" width=\"800\" height=\"800\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/grav_quiver.png 800w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/grav_quiver-300x300.png 300w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/grav_quiver-150x150.png 150w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/grav_quiver-768x768.png 768w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><\/a><\/p>\n<p>Just as we expected, the gravitational field is strongest near the sun. Gravity&#8217;s strength depends on the square of the distance between the bodies, so it&#8217;s very strong when you pass nearby a large object but falls off rapidly as you drift further away. The comet in the GIF experiences a strong force (the large arrows) and is sharply accelerated.<\/p>\n<p>I should point out that a gravitational field doesn&#8217;t have a fixed number of points like the grid of our plot. The field is continuous but we can&#8217;t plot an infinite number of arrows. We have to scale things down in order to visualize.<\/p>\n<p>This is also an intentionally simple example that includes only one celestial body. All the arrows point toward the sun. But it wouldn&#8217;t be too complicated to add more bodies. You would simply calculate the field as we did above and repeat the process for every body, then add the arrays together.<\/p>\n<p>As an example, here is a system with two equally massive stars:<\/p>\n<p><a href=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/grav_quiver_multiple.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-1932\" src=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/grav_quiver_multiple.png\" alt=\"\" width=\"800\" height=\"800\" srcset=\"https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/grav_quiver_multiple.png 800w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/grav_quiver_multiple-300x300.png 300w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/grav_quiver_multiple-150x150.png 150w, https:\/\/wollen.org\/blog\/wp-content\/uploads\/2025\/02\/grav_quiver_multiple-768x768.png 768w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><\/a><\/p>\n<p>Notice how the arrows tend to point toward the &#8220;average&#8221; of the two bodies. Directly between them, force vectors cancel each other out and there is minimal force applied. In a perfect system, some third body could hang out here without falling toward either star.<\/p>\n<p>I hope this post demystifies <code>meshgrid<\/code> to some extent. Obviously it&#8217;s capable of a lot more but understanding just this much opens up a whole world of two-dimensional data. You can create quiver plots, heat maps, contour plots, and more.<\/p>\n<hr \/>\n<p><a href=\"https:\/\/wollen.org\/misc\/grav_field_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\">import numpy as np\r\nimport matplotlib.pyplot as plt\r\n\r\n\r\nSTAR_POS_X = 20\r\nSTAR_POS_Y = 40\r\nSTAR_MASS = 4000\r\nCOMET_MASS = 100\r\nG_CONSTANT = 1.0\r\n\r\nx_grid, y_grid = np.meshgrid(range(-100, 105, 5),\r\n                             range(-100, 105, 5)\r\n                             )\r\n\r\ndistance_x = STAR_POS_X - x_grid\r\ndistance_y = STAR_POS_Y - y_grid\r\n\r\ndistance = np.sqrt(distance_x**2 + distance_y**2)\r\ndistance = np.ma.masked_array(distance, distance &lt;= 15)\r\n\r\nforce = G_CONSTANT * STAR_MASS * COMET_MASS \/ distance**2\r\n\r\nangle = np.atan2(distance_y, distance_x)\r\n\r\nx_components = force * np.cos(angle)\r\ny_components = force * np.sin(angle)\r\n\r\nplt.style.use(\"grav_field.mplstyle\")\r\n\r\nfig, ax = plt.subplots()\r\n\r\nax.quiver(x_grid, y_grid, x_components, y_components, pivot=\"tip\")\r\n\r\nax.scatter([STAR_POS_X], [STAR_POS_Y], color=\"#F5E342\", s=STAR_MASS, edgecolor=\"black\", linewidth=0.5)\r\n\r\nax.set(xticks=range(-100, 120, 20),\r\n       yticks=range(-100, 120, 20),\r\n       xlim=(-103, 103),\r\n       ylim=(-103, 103)\r\n       )\r\n\r\nplt.savefig(\"grav_quiver.png\")<\/pre>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Usually on the blog we apply a function to each element of a one-dimensional array, like [1, 2, 3], and get back another array, like [10, 20, 30]. Then we use both lists to plot a series of (x, y)<\/p>\n","protected":false},"author":1,"featured_media":1921,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[40,295],"tags":[299,387,82,22,382,296,24,383,388,126,75,297,306,25,386,385,384],"class_list":["post-1909","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-math","category-space","tag-acceleration","tag-animated","tag-array","tag-data","tag-gravitational-field","tag-gravity","tag-matplotlib","tag-matrix","tag-meshgrid","tag-mplstyle","tag-numpy","tag-physics","tag-position","tag-python","tag-quiver","tag-quiver-plot","tag-vector"],"_links":{"self":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/1909","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=1909"}],"version-history":[{"count":27,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/1909\/revisions"}],"predecessor-version":[{"id":2040,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/posts\/1909\/revisions\/2040"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/media\/1921"}],"wp:attachment":[{"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/media?parent=1909"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/categories?post=1909"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wollen.org\/blog\/wp-json\/wp\/v2\/tags?post=1909"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}