<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rat Wolf (ratwolf)</title>
    <description>The latest articles on DEV Community by Rat Wolf (ratwolf) (@ratwolf).</description>
    <link>https://dev.to/ratwolf</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2523543%2F4529e41c-c1dd-45fa-ad6b-ace947a2884c.jpg</url>
      <title>DEV Community: Rat Wolf (ratwolf)</title>
      <link>https://dev.to/ratwolf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ratwolf"/>
    <language>en</language>
    <item>
      <title>Hopalong Map – fast, parallel, low-memory – two-pass checkpointed orbit decomposition</title>
      <dc:creator>Rat Wolf (ratwolf)</dc:creator>
      <pubDate>Wed, 28 Oct 2026 12:32:25 +0000</pubDate>
      <link>https://dev.to/ratwolf/hopalong-map-fast-parallel-low-memory-two-pass-checkpointed-orbit-decomposition-23hm</link>
      <guid>https://dev.to/ratwolf/hopalong-map-fast-parallel-low-memory-two-pass-checkpointed-orbit-decomposition-23hm</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa77e5wgojc4v7c4m5ugc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa77e5wgojc4v7c4m5ugc.png" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Draws the &lt;strong&gt;Hopalong map&lt;/strong&gt; (Barry Martin's map) from hundreds of millions or even billions of points, using all CPU cores, &lt;strong&gt;with memory that does not grow with the number of points&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exact:&lt;/strong&gt; the picture is bit-for-bit identical to a plain, single-threaded implementation, and identical for any number of threads (checked with 1 and 8).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast:&lt;/strong&gt; all cores are used, and each core is kept busier than in a naive loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small:&lt;/strong&gt; memory stays fixed however many points you compute (about 76 MB on an 8-thread machine, on top of Python and its libraries).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two output flavours:&lt;/strong&gt; &lt;em&gt;counts&lt;/em&gt; (hits per pixel) or a proper &lt;em&gt;probability density&lt;/em&gt; on the colour bar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three colour scales:&lt;/strong&gt; linear, square root, logarithmic.&lt;/li&gt;
&lt;li&gt;Interactive window (matplotlib) plus PNG export.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://github.com/ratwolfzero/Hopalong_Map_Next_Gen" rel="noopener noreferrer"&gt;&lt;strong&gt;Github Repository&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
Hopalong Map – fast, parallel, low-memory – two-pass checkpointed orbit decomposition

&lt;ul&gt;
&lt;li&gt;Contents&lt;/li&gt;
&lt;li&gt;Quick start&lt;/li&gt;
&lt;li&gt;The Hopalong map&lt;/li&gt;
&lt;li&gt;The general approach (checkpoints and segments)&lt;/li&gt;
&lt;li&gt;The problem&lt;/li&gt;
&lt;li&gt;The idea in one picture&lt;/li&gt;
&lt;li&gt;Checkpoints (flags) and segments&lt;/li&gt;
&lt;li&gt;Why the result is exactly the same&lt;/li&gt;
&lt;li&gt;What is stored, and how much&lt;/li&gt;
&lt;li&gt;Lanes: keeping each core busy&lt;/li&gt;
&lt;li&gt;Where the time goes&lt;/li&gt;
&lt;li&gt;Counts vs. density&lt;/li&gt;
&lt;li&gt;Counts (default)&lt;/li&gt;
&lt;li&gt;Density&lt;/li&gt;
&lt;li&gt;What the density option is not&lt;/li&gt;
&lt;li&gt;Colour scales&lt;/li&gt;
&lt;li&gt;Using it from Python&lt;/li&gt;
&lt;li&gt;Configuration&lt;/li&gt;
&lt;li&gt;Code map&lt;/li&gt;
&lt;li&gt;Verification and benchmarks&lt;/li&gt;
&lt;li&gt;Limitations and notes&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Requirements:&lt;/strong&gt; Python 3 (developed with 3.15), &lt;code&gt;numpy&lt;/code&gt;, &lt;code&gt;numba&lt;/code&gt;, &lt;code&gt;matplotlib&lt;/code&gt;. Works on macOS and Linux (the memory read-out uses the Unix-only &lt;code&gt;resource&lt;/code&gt; module; on Windows remove &lt;code&gt;display_resource_usage&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;numpy numba matplotlib
python hopalong_fast.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script asks for six things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prompt&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, &lt;code&gt;c&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The three parameters of the map (any real numbers; &lt;code&gt;a = 0&lt;/code&gt; together with &lt;code&gt;c = 0&lt;/code&gt; is rejected because the orbit would never leave the origin).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;n&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Number of points to compute, at least 1000. Scientific notation such as &lt;code&gt;1e8&lt;/code&gt; is accepted.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Colour scale&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;linear&lt;/code&gt; (default), &lt;code&gt;sqrt&lt;/code&gt; or &lt;code&gt;log&lt;/code&gt; – see Colour scales.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Colour bar shows&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;counts&lt;/code&gt; (default) or &lt;code&gt;density&lt;/code&gt; – see Counts vs. density.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Example session (times and memory will differ on your machine; this one was run on an Apple MacMini M2):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Enter a float value for "a": -3.14
Enter a float value for "b": 0.3
Enter a float value for "c": 0.3
Enter a positive integer value &amp;gt; 1000 for "n": 1e8
Colour scale (linear / sqrt / log) [linear]: sqrt
Colour bar shows (counts / density) [counts]: density
Pass 1 (extents + checkpoints, serial): 0.81 s
Pass 2 (parallel plot):                 0.08 s
Wall-clock time (compute): 0.89 s
Memory (RAM): 221.25 MByte used (peak)
Check: sum(density) * cell area = 1.000000
Image saved to: &amp;lt;OUTPUT_DIR&amp;gt;/py_num=100000000_a=-3.14_b=0.3_c=0.3_sqrt_density.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a window opens with the picture, and a PNG is written to &lt;code&gt;OUTPUT_DIR&lt;/code&gt;. &lt;strong&gt;Change &lt;code&gt;OUTPUT_DIR&lt;/code&gt; at the top of the script to a folder that exists on your machine.&lt;/strong&gt; The PNG is the raw 1000 × 1000 picture without axes; the file name encodes the parameters, the colour scale (&lt;code&gt;_sqrt&lt;/code&gt;, &lt;code&gt;_log&lt;/code&gt;) and the mode (&lt;code&gt;_density&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The first start takes a few seconds because numba compiles the kernels; they are cached on disk afterwards.&lt;/p&gt;

&lt;p&gt;The "Memory" line is the peak of the whole Python process, including Python, numba and matplotlib themselves (roughly 150 MB before anything is computed).&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hopalong map
&lt;/h2&gt;

&lt;p&gt;Every point is computed from the previous one. Starting at the origin, (x, y) = (0, 0):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9z1174celv4va7zbygxx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9z1174celv4va7zbygxx.png" width="688" height="135"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr93qmwjr1di1ez56srlp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr93qmwjr1di1ez56srlp.png" width="339" height="117"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;where &lt;code&gt;sign(x)&lt;/code&gt; is +1 for positive and -1 for negative &lt;code&gt;x&lt;/code&gt;. The three numbers &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, &lt;code&gt;c&lt;/code&gt; decide what the picture looks like; for example &lt;code&gt;a = -3.14&lt;/code&gt;, &lt;code&gt;b = 0.3&lt;/code&gt;, &lt;code&gt;c = 0.3&lt;/code&gt; gives a ring-and-petal pattern.&lt;/p&gt;

&lt;p&gt;Two facts about this map drive the whole design:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It is strictly serial.&lt;/strong&gt; Point number 1,000,000 cannot be computed without first computing points 1 to 999,999. You cannot simply hand different parts of the orbit to different CPU cores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is deterministic.&lt;/strong&gt; The same &lt;code&gt;(x, y)&lt;/code&gt; always leads to the same next point. There is no randomness. If you know where you are, you know everything that follows.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;A note on "map" versus "attractor".&lt;/strong&gt; The Hopalong map is often called an attractor, but it does not pull orbits in from elsewhere. Away from the lines &lt;code&gt;x = 0&lt;/code&gt; and &lt;code&gt;b * x = c&lt;/code&gt;, its Jacobian determinant (the factor by which the map stretches areas) is exactly 1, so the map &lt;strong&gt;preserves area&lt;/strong&gt;. You can check this by hand: the new &lt;code&gt;x&lt;/code&gt; changes with &lt;code&gt;y&lt;/code&gt; at slope 1, and the new &lt;code&gt;y&lt;/code&gt; changes with &lt;code&gt;x&lt;/code&gt; at slope -1 and does not depend on &lt;code&gt;y&lt;/code&gt;, so the determinant is 1. Orbits therefore stay in regions that are determined by where they start, and different start points generally give different pictures. &lt;strong&gt;The picture is the orbit from &lt;code&gt;(0, 0)&lt;/code&gt;, not a property of &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt; and &lt;code&gt;c&lt;/code&gt; alone.&lt;/strong&gt; That is why the exact orbit is replayed from checkpoints, instead of starting the segments at independent random points.&lt;/p&gt;




&lt;h2&gt;
  
  
  The general approach (checkpoints and segments)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The problem
&lt;/h3&gt;

&lt;p&gt;To draw the picture you lay a grid over the plane (here 1000 × 1000 cells) and, for every point of the orbit, add one tally mark to the cell it lands in. Cells that were visited often end up bright. Such a grid of tally counts is called a &lt;strong&gt;histogram&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Two things make this harder than it sounds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You must know the size of the picture before you can start tallying.&lt;/strong&gt; Where is the leftmost point, the topmost? That is only known after the whole orbit has been walked. This is why every implementation needs &lt;em&gt;two passes&lt;/em&gt;: one to measure, one to draw.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storing the orbit is out of the question.&lt;/strong&gt; One point is two decimal numbers, 16 bytes. A billion points would be 16 GB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A plain implementation therefore walks the orbit twice, one after the other, on a single core.&lt;/p&gt;

&lt;h3&gt;
  
  
  The idea in one picture
&lt;/h3&gt;

&lt;p&gt;Think of the orbit as a very long hiking trail where each step depends on the previous one, so one hiker has to walk it in order. The trick:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pass 1:&lt;/strong&gt; one hiker walks the whole trail once. On the way they note the extreme positions (how big must the map be?) and &lt;strong&gt;plant a flag every so often&lt;/strong&gt;. A flag records just the exact position at that spot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass 2:&lt;/strong&gt; now &lt;em&gt;many&lt;/em&gt; hikers start at the same time, &lt;strong&gt;each at a different flag&lt;/strong&gt;, and each walks only the stretch up to the next flag. Because the trail is deterministic, they all take exactly the steps the single hiker would have taken. Each hiker makes tally marks on &lt;strong&gt;their own private tally sheet&lt;/strong&gt;, so nobody gets in anybody's way. At the end, the tally sheets are added up.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; PASS 1  -  one worker walks the whole orbit once (serial)
   |         remembers the smallest and largest x and y   -&amp;gt; how big is the picture?
   |         plants a flag at the start of every segment  -&amp;gt; a few bytes each
   v
 PASS 2  -  many workers, all at the same time (parallel)

   worker 1:  walks segments 0-3    -&amp;gt; marks its own tally sheet 1 --+
   worker 2:  walks segments 4-7    -&amp;gt; marks its own tally sheet 2 --+
   worker 3:  walks segments 8-11   -&amp;gt; marks its own tally sheet 3 --+--&amp;gt; add up all
   ...                                                               |    tally sheets
   worker k:  walks segments ...    -&amp;gt; marks its own tally sheet k --+        |
                                                                              v
                                                                       final picture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Checkpoints (flags) and segments
&lt;/h3&gt;

&lt;p&gt;The orbit is cut into consecutive pieces called &lt;strong&gt;segments&lt;/strong&gt;. A &lt;strong&gt;checkpoint&lt;/strong&gt; is the exact position &lt;code&gt;(x, y)&lt;/code&gt; at the start of a segment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   flag 0        flag 1        flag 2        flag 3        flag 4
     |             |             |             |             |
     v             v             v             v             v
     +-------------+-------------+-------------+-------------+--------- ...
     |  segment 0  |  segment 1  |  segment 2  |  segment 3  |  segment 4
     +-------------+-------------+-------------+-------------+--------- ...
   start (0,0)                                                    the orbit goes on
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;A worker that starts at flag &lt;em&gt;k&lt;/em&gt; just keeps computing until it reaches the position stored in flag &lt;em&gt;k+1&lt;/em&gt;, adding a tally mark for every point on the way.&lt;/li&gt;
&lt;li&gt;If the number of points does not divide evenly into segments, the leftover points are simply added to the last segment, so every point is visited exactly once.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why the result is exactly the same
&lt;/h3&gt;

&lt;p&gt;Starting from an exact checkpoint, a worker repeats &lt;strong&gt;exactly the same arithmetic&lt;/strong&gt; as the single hiker did, so it produces exactly the same numbers. The set of tally marks is the same as in a single sequential run, only the order in which they are made differs, and adding up whole numbers does not depend on the order. So the picture is identical, no matter how many cores are used.&lt;/p&gt;

&lt;p&gt;This matters more than it may seem: the map is chaotic, so even a difference in the 16th decimal place would grow until the replayed orbit looked completely different. Both passes therefore use the very same step function and plain IEEE floating-point arithmetic (do &lt;strong&gt;not&lt;/strong&gt; switch on numba's &lt;code&gt;fastmath&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  What is stored, and how much
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Depends on &lt;code&gt;n&lt;/code&gt;?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The orbit itself&lt;/td&gt;
&lt;td&gt;not stored at all&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkpoints&lt;/td&gt;
&lt;td&gt;16 bytes per segment, about 1 KB in total&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One tally sheet per task&lt;/td&gt;
&lt;td&gt;4 MB each (1000 × 1000 counters of 4 bytes)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final picture&lt;/td&gt;
&lt;td&gt;8 MB&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With &lt;em&gt;T&lt;/em&gt; CPU threads the script uses &lt;code&gt;2 × T&lt;/code&gt; tasks and &lt;code&gt;4&lt;/code&gt; segments per task, so &lt;code&gt;8 × T&lt;/code&gt; segments. On an 8-thread machine that is 64 segments, i.e. &lt;strong&gt;1 KB of checkpoints&lt;/strong&gt;, instead of 16 GB for a billion stored points. The only memory that grows is the number of tally sheets (one per task), and it grows with the number of cores and with the picture resolution, never with &lt;code&gt;n&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The counters of a tally sheet are 4-byte numbers, which overflow at about 4.3 billion. To be safe, no single task is allowed to plot more than 2 billion points (&lt;code&gt;MAX_POINTS_PER_TASK&lt;/code&gt;); for very large &lt;code&gt;n&lt;/code&gt; the script simply uses more tasks. The sheets are then added up in 8-byte numbers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lanes: keeping each core busy
&lt;/h3&gt;

&lt;p&gt;Each step needs the result of the previous step, including a square root, which takes the CPU a moment. A core working on one chain of steps spends part of its time waiting. So each task works on &lt;strong&gt;four consecutive segments in lock-step&lt;/strong&gt; ("lanes"): while the CPU waits for the square root of lane 1, it already works on lanes 2, 3 and 4. This uses no extra cores, it just makes each core busier. In the benchmark below this makes pass 2 about 3.7 times faster &lt;em&gt;per core&lt;/em&gt; (0.82 s down to 0.22 s on an Apple M2; the gain depends on the CPU).&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the time goes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Pass 1 is serial: it costs roughly as much as the walk itself.&lt;/li&gt;
&lt;li&gt;Pass 2 is parallel: its time shrinks with the number of cores.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the total time is roughly &lt;code&gt;pass 1 + pass 2 / number of cores&lt;/code&gt;. Beyond a handful of cores, pass 1 dominates, and extra cores no longer help much. The script prints both times so you can see this. On an 8-thread Apple M2, pass 1 takes 0.81 s of the 0.87 s total (see the benchmark below).&lt;/p&gt;




&lt;h2&gt;
  
  
  Counts vs. density
&lt;/h2&gt;

&lt;p&gt;Both options run the &lt;strong&gt;same two passes&lt;/strong&gt; and show practically the same picture. They differ in how points are assigned to cells and in what the colour bar says.&lt;/p&gt;

&lt;h3&gt;
  
  
  Counts (default)
&lt;/h3&gt;

&lt;p&gt;The colour bar shows &lt;strong&gt;how many points landed in each pixel&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A point is assigned to the &lt;em&gt;nearest grid point&lt;/em&gt;, where the grid points lie exactly on the minimum and maximum of the orbit. With &lt;code&gt;W&lt;/code&gt; columns:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fifftwmppdpd653fvtiqg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fifftwmppdpd653fvtiqg.png" width="791" height="183"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(rows work the same way with &lt;code&gt;y&lt;/code&gt; and &lt;code&gt;H&lt;/code&gt;). This reproduces the output of the plain single-threaded two-pass implementation exactly. It has one property that makes it unsuitable for a density: the first and last row and column only collect points from half a pixel, so they are only half as wide as all other cells.&lt;/p&gt;

&lt;h3&gt;
  
  
  Density
&lt;/h3&gt;

&lt;p&gt;The colour bar shows the &lt;strong&gt;probability density&lt;/strong&gt;: how concentrated the orbit's points are per unit area of the x-y plane. For this the grid must consist of &lt;strong&gt;equal-width cells&lt;/strong&gt;, exactly like &lt;code&gt;numpy.histogram2d&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmgwncvisvoa59b41sal5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmgwncvisvoa59b41sal5.png" width="568" height="183"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(a point exactly on the right edge goes into the last cell, as in numpy). With the cell sizes&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbewsr3i8avgg4hd1m5rs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbewsr3i8avgg4hd1m5rs.png" width="798" height="152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;the density in cell &lt;em&gt;(i, j)&lt;/em&gt;, with &lt;code&gt;n_ij&lt;/code&gt; hits and &lt;code&gt;N&lt;/code&gt; points in total, is&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flle42fsr4apk9w0jsdtz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flle42fsr4apk9w0jsdtz.png" width="366" height="166"&gt;&lt;/a&gt;&lt;br&gt;
Added up over the whole area, the density gives exactly 1:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2j0qh7i6rgp7y3zqrpzs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2j0qh7i6rgp7y3zqrpzs.png" width="592" height="188"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The script prints this as &lt;code&gt;Check: sum(density) * cell area = 1.000000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two points are worth stressing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The density is just the counts times one constant&lt;/strong&gt;, &lt;code&gt;1 / (N * dx * dy)&lt;/code&gt;. The counts stay exact whole numbers; nothing is rounded, smoothed or re-binned when converting. So the picture is the same and only the numbers on the colour bar change. (&lt;code&gt;N&lt;/code&gt; equals the &lt;code&gt;n&lt;/code&gt; you typed, because the extents were measured on the very same orbit and therefore every point falls inside the grid.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is exactly what &lt;code&gt;numpy.histogram2d(x, y, bins=..., density=True)&lt;/code&gt; would give&lt;/strong&gt; for the same points and the same range, only without ever holding the points in memory. Counting into bins is also all &lt;code&gt;histogram2d&lt;/code&gt; does; the binning loses exactly the same information in both cases.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  What the density option is not
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not the exact density of the map.&lt;/strong&gt; It is a &lt;em&gt;histogram estimate&lt;/em&gt; on a 1000 × 1000 grid. For many parameter sets the points pile up on thin curves and rings; for such patterns the density value in a cell depends on how large the cell is, and a finer grid gives higher values on the curves. There may be no resolution-independent "true" value to converge to. A larger &lt;code&gt;n&lt;/code&gt; reduces the noise (cells with few hits are noisy), but it does not remove the dependence on the cell size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a probability per pixel.&lt;/strong&gt; It is a probability &lt;em&gt;per unit area&lt;/em&gt;. The probability that a point falls into one cell is &lt;code&gt;n_ij / N&lt;/code&gt;, which is the density multiplied by the cell area:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4fc8ibs9883ywdadj9oh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4fc8ibs9883ywdadj9oh.png" width="494" height="158"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not independent random samples.&lt;/strong&gt; The points come from one deterministic orbit and are strongly correlated. The numbers describe how often &lt;em&gt;this&lt;/em&gt; orbit, started at &lt;code&gt;(0, 0)&lt;/code&gt;, visits each cell; since the map preserves area (see The Hopalong map), other start points give different pictures. Textbook error estimates for independent samples (such as the square root of the count) are only a rough guide here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not smoothed.&lt;/strong&gt; There is no kernel density estimate and no blurring; every cell holds the raw tally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not normalised to a maximum of 1.&lt;/strong&gt; The numbers on the colour bar are real density values. Only the &lt;em&gt;colours&lt;/em&gt; are scaled relative to the busiest cell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not affected by the colour scale.&lt;/strong&gt; &lt;code&gt;sqrt&lt;/code&gt; and &lt;code&gt;log&lt;/code&gt; change which colour a value gets, never the numbers on the colour bar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not stored in the PNG.&lt;/strong&gt; The PNG contains colours, not data. To get the density values themselves, use the Python interface below.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Colour scales
&lt;/h2&gt;

&lt;p&gt;A pixel's relative brightness (0 to 1) is derived from its hit count &lt;code&gt;n&lt;/code&gt; and the highest hit count &lt;code&gt;n_max&lt;/code&gt; in the picture. The scale only decides how counts are mapped to colours; the colour bar ticks always show the real counts (or densities).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;linear&lt;/strong&gt; – proportional to the count. Rarely visited regions end up (almost) black:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F88ruax2qbcdlt8ea7nkd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F88ruax2qbcdlt8ea7nkd.png" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;sqrt&lt;/strong&gt; – lifts sparse regions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgpumshjjkwacxj8v6hvl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgpumshjjkwacxj8v6hvl.png" width="359" height="185"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;log&lt;/strong&gt; – strongest lift, even single hits become visible:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flg0cfl835496g28l59hn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flg0cfl835496g28l59hn.png" width="456" height="182"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The tick labels are spaced evenly along the colour bar, not evenly in count value, so with &lt;code&gt;log&lt;/code&gt; you see labels like 0, 1, 3, 8, 17, 36, ...&lt;/p&gt;


&lt;h2&gt;
  
  
  Using it from Python
&lt;/h2&gt;

&lt;p&gt;The file can be imported; nothing runs on import.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hopalong_fast&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;hf&lt;/span&gt;

&lt;span class="n"&gt;hf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warm_up&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# compile, or load the cached kernels, once
&lt;/span&gt;
&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;3.14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100_000_000&lt;/span&gt;

&lt;span class="c1"&gt;# uniform_bins=True -&amp;gt; equal-width cells, as needed for a density
&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t_pass1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t_pass2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compute_trajectory_image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;uniform_bins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;density&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;hf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;density_factor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# integrates to 1 over the area
&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;density.npy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;density&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;counts&lt;/code&gt; is an unsigned 64-bit array of shape &lt;code&gt;(rows, columns)&lt;/code&gt; = &lt;code&gt;(y, x)&lt;/code&gt;. Row 0 is the &lt;strong&gt;lowest&lt;/strong&gt; y value. &lt;code&gt;numpy.histogram2d&lt;/code&gt; returns shape &lt;code&gt;(x, y)&lt;/code&gt;, so compare with &lt;code&gt;density.T&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;extents&lt;/code&gt; is &lt;code&gt;(x_min, x_max, y_min, y_max)&lt;/code&gt;. The cell edges are &lt;code&gt;np.linspace(x_min, x_max, columns + 1)&lt;/code&gt; and &lt;code&gt;np.linspace(y_min, y_max, rows + 1)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;With &lt;code&gt;uniform_bins=False&lt;/code&gt; (the default) you get the pixel-mode counts of the plain single-threaded two-pass implementation.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;Constants at the top of &lt;code&gt;hopalong_fast.py&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constant&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LANES&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Segments advanced in lock-step per task. The kernel is written out for exactly 4 lanes; do not change this number without rewriting &lt;code&gt;second_pass&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PARALLEL_THRESHOLD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1_000_000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Below this many points a single task is used; the parallel set-up would cost more than it saves.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MAX_POINTS_PER_TASK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1 &amp;lt;&amp;lt; 31&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Upper bound on the points one task plots, so that its 4-byte counters cannot overflow.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OUTPUT_DIR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/Users/ralf/Projects/hopalong_pictures&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Where PNG files are written. &lt;strong&gt;Change this.&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SCALES&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;linear&lt;/code&gt;, &lt;code&gt;sqrt&lt;/code&gt;, &lt;code&gt;log&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Allowed colour scales.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OUTPUTS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;counts&lt;/code&gt;, &lt;code&gt;density&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Allowed colour-bar modes.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Other knobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Number of threads:&lt;/strong&gt; set the environment variable &lt;code&gt;NUMBA_NUM_THREADS&lt;/code&gt;, for example &lt;code&gt;NUMBA_NUM_THREADS=4 python hopalong_fast.py&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolution and colour map:&lt;/strong&gt; &lt;code&gt;main(image_size=(1000, 1000), color_map='hot')&lt;/code&gt;. Every task needs &lt;code&gt;rows × columns × 4&lt;/code&gt; bytes, so memory grows with the number of tasks and with the resolution: at 4000 × 4000 each tally sheet is 64 MB.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Code map
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;make_plan(n)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Chooses the number of tasks and cuts &lt;code&gt;n&lt;/code&gt; points into segments.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_step&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One Hopalong iteration. The same function is used in both passes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;first_pass&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pass 1: walks the orbit serially; returns the extents and the checkpoints.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_hit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maps a point to its cell (pixel mode or equal-width mode) and counts it. Points outside the grid, or NaN/infinite points, are discarded.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;second_pass&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pass 2: parallel over tasks (&lt;code&gt;prange&lt;/code&gt;), four lanes per task, one private tally sheet per task.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compute_trajectory_image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runs both passes, sums the tally sheets, returns counts, extents and timings.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;density_factor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The constant &lt;code&gt;1 / (N * dx * dy)&lt;/code&gt; that turns counts into a density.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;make_norm&lt;/code&gt;, &lt;code&gt;colorbar&lt;/code&gt;, &lt;code&gt;render_trajectory_image&lt;/code&gt;, &lt;code&gt;save_png&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Colour scales, colour bar, window and PNG output.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Verification and benchmarks
&lt;/h2&gt;

&lt;p&gt;Measured on: Apple M2, 8 threads, 8 GB RAM, macOS (Darwin 27.2.0), Python 3.15.1, numpy 2.5.4, numba 0.68.0.&lt;/p&gt;

&lt;p&gt;All numbers in this section are produced by &lt;code&gt;bench_readme.py&lt;/code&gt;; run &lt;code&gt;python bench_readme.py&lt;/code&gt; to reproduce them (add &lt;code&gt;--big&lt;/code&gt; for the 100-million-point memory test).&lt;/p&gt;

&lt;p&gt;Checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Counts mode vs. the plain single-threaded two-pass implementation:&lt;/strong&gt; the extents and the complete 1000 × 1000 image are &lt;strong&gt;bit-for-bit identical&lt;/strong&gt; for 4 different parameter sets, including values of &lt;code&gt;n&lt;/code&gt; that do not divide evenly into segments and runs with several tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Density mode vs. &lt;code&gt;numpy.histogram2d(..., density=True)&lt;/code&gt;:&lt;/strong&gt; with 2 to 3 million points on 3 parameter sets, &lt;strong&gt;no cell differs&lt;/strong&gt; in its count, the densities agree to floating-point rounding (relative difference about 2e-13), and the integral is within 5e-14 of 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Colour scales:&lt;/strong&gt; the saved PNGs were compared with the three formulas above (using a grey colour map) and agree to within 2 grey levels out of 255, which is the rounding of a 256-entry colour map and 8-bit PNG; the orientation (y pointing up) was checked at the same time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Speed, 100,000,000 points, parameters a = 0.4, b = 1.0, c = 0.0, 1000 × 1000 grid, median of 3 runs. Both versions were timed on this machine in the same session; the plain loop is single-threaded:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Pass 1&lt;/th&gt;
&lt;th&gt;Pass 2&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plain two-pass numba loop (single-threaded --&amp;gt;"hopalong_basic.py")&lt;/td&gt;
&lt;td&gt;0.80 s&lt;/td&gt;
&lt;td&gt;0.82 s&lt;/td&gt;
&lt;td&gt;1.62 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;hopalong_fast.py&lt;/code&gt;, 1 thread&lt;/td&gt;
&lt;td&gt;0.81 s&lt;/td&gt;
&lt;td&gt;0.22 s&lt;/td&gt;
&lt;td&gt;1.02 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;hopalong_fast.py&lt;/code&gt;, 8 threads&lt;/td&gt;
&lt;td&gt;0.81 s&lt;/td&gt;
&lt;td&gt;0.06 s&lt;/td&gt;
&lt;td&gt;0.87 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Memory, density histogram of the same orbit (extra peak RAM on top of what the interpreter and the libraries already use; &lt;code&gt;hopalong_fast.py&lt;/code&gt; with 8 threads = 16 tasks, so at most 16 × 4 MB of tally sheets plus the 8 MB result; the operating system only counts the parts that were actually touched):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Extra peak RAM, 20,000,000 points&lt;/th&gt;
&lt;th&gt;100,000,000 points&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generate the points into arrays, then &lt;code&gt;numpy.histogram2d&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;1.1 GB&lt;/td&gt;
&lt;td&gt;not run (estimated at about 6 GB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hopalong_fast.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;76 MB&lt;/td&gt;
&lt;td&gt;76 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Limitations and notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pass 1 is serial.&lt;/strong&gt; That is the floor for the total time; see Where the time goes. It cannot simply be skipped by starting segments at independent random points, because the picture depends on the start point (see The Hopalong map).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First start compiles.&lt;/strong&gt; numba compiles the kernels on first use (a few seconds) and caches them in &lt;code&gt;__pycache__&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not independent samples, and not a continuous density.&lt;/strong&gt; See What the density option is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extreme parameters.&lt;/strong&gt; If the orbit overflows to infinity or NaN, the extents and the picture are meaningless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not enable &lt;code&gt;fastmath&lt;/code&gt;.&lt;/strong&gt; Pass 1 and pass 2 must compute bit-identical orbits for the checkpoints to be valid (the map is chaotic, see Why the result is exactly the same).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform.&lt;/strong&gt; The memory read-out uses the Unix-only &lt;code&gt;resource&lt;/code&gt; module (macOS and Linux). Peak memory is reported in the unit appropriate for each platform.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>algorithms</category>
      <category>performance</category>
      <category>programming</category>
      <category>python</category>
    </item>
    <item>
      <title>One-pass Hopalong orbit density plot</title>
      <dc:creator>Rat Wolf (ratwolf)</dc:creator>
      <pubDate>Sun, 25 Oct 2026 07:05:26 +0000</pubDate>
      <link>https://dev.to/ratwolf/one-pass-hopalong-orbit-density-plot-1eik</link>
      <guid>https://dev.to/ratwolf/one-pass-hopalong-orbit-density-plot-1eik</guid>
      <description>&lt;p&gt;This script renders the classic Hopalong map as a visit-density image. It&lt;br&gt;
streams the orbit into a fixed-size histogram and adapts the represented&lt;br&gt;
coordinate range when necessary, rather than storing every orbit point or&lt;br&gt;
making a full preliminary pass to discover the bounds.&lt;/p&gt;
&lt;h2&gt;
  
  
  Target
&lt;/h2&gt;

&lt;p&gt;The goal is to plot a long orbit with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Memory bounded by the histogram dimensions, rather than the number of
iterations.&lt;/li&gt;
&lt;li&gt;Exact integer visit counts at the histogram's &lt;strong&gt;final&lt;/strong&gt; cell resolution.&lt;/li&gt;
&lt;li&gt;Automatic coverage when the orbit moves beyond the current grid, at the
finest cell size that still fits the whole orbit.&lt;/li&gt;
&lt;li&gt;A useful density visualization, optionally Gaussian-smoothed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The implementation uses NumPy arrays, Matplotlib, and Numba. Save the script&lt;br&gt;
as &lt;code&gt;hopalong_orbit_onepass.py&lt;/code&gt; or substitute its actual filename in the&lt;br&gt;
commands below.&lt;/p&gt;
&lt;h2&gt;
  
  
  Implementation details
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Orbit recurrence
&lt;/h3&gt;

&lt;p&gt;Starting at &lt;code&gt;(x, y) = (0, 0)&lt;/code&gt;, each step uses the previous &lt;code&gt;x&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;old_x = x
x = y - sign(old_x) * sqrt(abs(b * old_x - c))
y = a - old_x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default parameters are &lt;code&gt;a=0.7&lt;/code&gt;, &lt;code&gt;b=0.4&lt;/code&gt;, and &lt;code&gt;c=0&lt;/code&gt;. The first &lt;code&gt;--skip&lt;/code&gt;&lt;br&gt;
steps are treated as a transient and are not added to the histogram.&lt;/p&gt;
&lt;h3&gt;
  
  
  Starting grid
&lt;/h3&gt;

&lt;p&gt;The first 4,096 plotted points are held in a small buffer (64 KB). Their&lt;br&gt;
bounding box selects the starting cell size: the smallest power of two times&lt;br&gt;
&lt;code&gt;H_MIN = 2**-32&lt;/code&gt; at which the box fits the grid. The grid is centred on the&lt;br&gt;
box, the buffered points are binned, and the orbit simply continues from where&lt;br&gt;
it was. No recurrence step is computed twice, and there is no separate pilot&lt;br&gt;
orbit.&lt;/p&gt;

&lt;p&gt;This is a sizing heuristic only. Later points are still checked, and the grid&lt;br&gt;
is reframed whenever a point falls outside it. The attractor's extent usually&lt;br&gt;
keeps growing with the number of points, so the buffer cannot predict the final&lt;br&gt;
size; it only avoids a burst of cheap-but-pointless regridding at the start.&lt;/p&gt;
&lt;h3&gt;
  
  
  Streaming histogram and dyadic growth
&lt;/h3&gt;

&lt;p&gt;The histogram is a square &lt;code&gt;size x size&lt;/code&gt; array of signed 64-bit integer&lt;br&gt;
counts. A point inside the current bounds is mapped to a cell by scaling its&lt;br&gt;
coordinates by the inverse cell width and converting to integer indices. The&lt;br&gt;
hot loop does nothing else: it carries no bookkeeping of its own.&lt;/p&gt;

&lt;p&gt;When a plotted point falls outside the grid, the grid is reframed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The occupied block of cells is located with one pass over the array.&lt;/li&gt;
&lt;li&gt;If the occupied block &lt;strong&gt;and&lt;/strong&gt; the new point still fit at the current cell
size, the counts are only &lt;strong&gt;translated&lt;/strong&gt;, so the block is centred. Nothing
is merged and no resolution is lost.&lt;/li&gt;
&lt;li&gt;Otherwise the cell width is doubled: each 2x2 block of counts is merged into
one cell by integer addition, and the merged block is centred in the grid.
This repeats until everything fits.&lt;/li&gt;
&lt;li&gt;The triggering point is binned and orbit processing resumes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Centring leaves room on both sides, so a reframe is rarely needed again soon.&lt;br&gt;
Only the occupied block is read, written and cleared, using two preallocated&lt;br&gt;
arrays that swap roles, so a reframe costs a few milliseconds at 2048 x 2048.&lt;/p&gt;

&lt;p&gt;All cell widths are &lt;code&gt;H_MIN * 2**k&lt;/code&gt;, so old and new cell boundaries stay&lt;br&gt;
aligned: translating and 2x2 merging are exact aggregations onto the new&lt;br&gt;
lattice. Earlier points are not lost or approximated in count. However, their&lt;br&gt;
original fine-cell locations cannot be recovered after merging: all points are&lt;br&gt;
ultimately represented at the final, coarser resolution.&lt;/p&gt;

&lt;p&gt;Because the grid doubles only when the occupied region no longer fits, the&lt;br&gt;
final occupied region covers between about 50% and 100% of the grid side.&lt;/p&gt;
&lt;h3&gt;
  
  
  Output image
&lt;/h3&gt;

&lt;p&gt;Empty outer rows and columns are cropped before display. By default the&lt;br&gt;
script displays &lt;code&gt;log1p(count)&lt;/code&gt; with a &lt;code&gt;magma&lt;/code&gt; color map. With &lt;code&gt;--smooth&lt;/code&gt;, it&lt;br&gt;
applies a separable Gaussian filter with sigma 1.5 cells before taking the&lt;br&gt;
logarithm. &lt;code&gt;--save&lt;/code&gt; writes &lt;code&gt;hopalong_orbit.png&lt;/code&gt; to the selected output&lt;br&gt;
directory instead of opening a window.&lt;/p&gt;
&lt;h3&gt;
  
  
  Tuning resolution with &lt;code&gt;--bins&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;--bins&lt;/code&gt; to set the side length of the square count grid. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python hopalong_orbit_onepass.py &lt;span class="nt"&gt;--bins&lt;/span&gt; 4096 &lt;span class="nt"&gt;--save&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Larger values generally produce a smaller final &lt;code&gt;cell&lt;/code&gt; width in the status&lt;br&gt;
output, meaning finer spatial resolution. The relationship is not perfectly&lt;br&gt;
linear: the grid only doubles its cell width when the orbit outgrows it, so&lt;br&gt;
the final cell width also depends on the orbit's coordinate range. Compare&lt;br&gt;
&lt;code&gt;cell&lt;/code&gt; values for the same parameters and iteration count; a smaller value&lt;br&gt;
means finer bins.&lt;/p&gt;

&lt;p&gt;The memory cost grows quadratically with &lt;code&gt;--bins&lt;/code&gt;. The two 64-bit count grids&lt;br&gt;
alone use about &lt;code&gt;16 * bins * bins&lt;/code&gt; bytes: approximately 64 MiB at 2048 and&lt;br&gt;
256 MiB at 4096, before temporary arrays used for plotting or smoothing. The&lt;br&gt;
option is rounded up to an even grid side internally because the grid growth&lt;br&gt;
merges cells in 2x2 blocks.&lt;/p&gt;
&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;The result is a two-dimensional image of how often the orbit visits each&lt;br&gt;
cell. Dense regions appear brighter, making the orbit's spatial structure&lt;br&gt;
visible without keeping the full point sequence in memory.&lt;/p&gt;

&lt;p&gt;On normal completion, the script reports the number of binned points, grid&lt;br&gt;
size, final cell width, number of grid doublings, elapsed time, and occupied&lt;br&gt;
image dimensions and coordinate extent. If the orbit reaches the escape&lt;br&gt;
threshold, processing stops and the report indicates that it escaped; fewer&lt;br&gt;
than the requested number of points may then be plotted. If it escapes before&lt;br&gt;
any point is plotted, the script exits with a message.&lt;/p&gt;
&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resolution decreases on growth.&lt;/strong&gt; Each doubling of the coordinate span
also doubles the cell width. This preserves a fixed-size grid, but an orbit
whose extent keeps growing with the number of points ends up with a coarser
final image the longer it runs. The cell width is the smallest power of two
at which the whole occupied region fits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Counts are exact only for the final bins.&lt;/strong&gt; Integer merges preserve visit
totals, but do not preserve sub-cell positions or allow a later
higher-resolution reconstruction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Floating-point bin edges.&lt;/strong&gt; Orbit coordinates and grid-to-cell mapping
use floating-point arithmetic. Points extremely close to a cell boundary
can be assigned to either neighboring cell due to rounding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reframing has a small fixed cost.&lt;/strong&gt; Each reframe scans the grid once and
moves only the occupied block. Typical runs need zero to a few dozen
reframes, and heavy-tailed orbits somewhat more; the total is on the order of
0.1 s at 2048 x 2048 and does not scale with the number of iterations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finer images touch more memory.&lt;/strong&gt; A sharper final grid has more occupied
cells, so the scattered histogram updates miss the cache a little more often.
Measured on one machine, this was a few percent, and it is the price of the
better resolution rather than of the reframing code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory is bounded by grid size, not necessarily small.&lt;/strong&gt; Two count arrays
are kept (about &lt;code&gt;16 * bins * bins&lt;/code&gt; bytes in total), plus temporary memory for
plotting/smoothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finest cell size.&lt;/strong&gt; Cells never get finer than &lt;code&gt;2**-32&lt;/code&gt;. This only matters
for orbits whose entire extent is below about &lt;code&gt;5e-7&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Escape guard.&lt;/strong&gt; The orbit stops if either coordinate's absolute value
exceeds &lt;code&gt;1e6&lt;/code&gt;. This is a safety cutoff, not a general mathematical test
that every parameter choice produces a bounded orbit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smoothing changes the displayed density.&lt;/strong&gt; Gaussian smoothing is a
visualization step, not part of the visit counts. Its sigma is measured in
cells, so the apparent blur depends on the final cell size. Zero padding can
reduce smoothed values near the image boundary.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Use cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Exploring the spatial density and structure of Hopalong orbits.&lt;/li&gt;
&lt;li&gt;Quickly comparing plots produced with different &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, and &lt;code&gt;c&lt;/code&gt;
parameters.&lt;/li&gt;
&lt;li&gt;Producing a bounded-memory visualization for very long runs when a
fixed-resolution image is acceptable.&lt;/li&gt;
&lt;li&gt;Creating a smoothed, presentation-friendly density image with &lt;code&gt;--smooth&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This method is intended for visualization, not for retaining a high&lt;br&gt;
precision point cloud or supporting later zooming and re-binning. For those&lt;br&gt;
needs, store orbit points or use a sparse/finer-resolution representation,&lt;br&gt;
accepting the associated memory or processing cost.&lt;/p&gt;
&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;p&gt;Install the dependencies in the Python environment used to run the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;numpy matplotlib numba
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python hopalong_orbit_onepass.py
python hopalong_orbit_onepass.py &lt;span class="nt"&gt;--smooth&lt;/span&gt;
python hopalong_orbit_onepass.py &lt;span class="nt"&gt;--save&lt;/span&gt;
python hopalong_orbit_onepass.py &lt;span class="nt"&gt;--a&lt;/span&gt; 1.1 &lt;span class="nt"&gt;--b&lt;/span&gt; 0.5 &lt;span class="nt"&gt;--c&lt;/span&gt; 0.3 &lt;span class="nt"&gt;--iters&lt;/span&gt; 2000000 &lt;span class="nt"&gt;--save&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--bins&lt;/code&gt; sets the square grid side length. If omitted, the script derives it&lt;br&gt;
from &lt;code&gt;--iters&lt;/code&gt; and caps it at 2,048; the resulting side length is made even&lt;br&gt;
for the 2x2 merge operation. Increase it for finer resolution at greater&lt;br&gt;
memory cost; check the reported &lt;code&gt;cell&lt;/code&gt; value to compare the actual final&lt;br&gt;
resolution. &lt;code&gt;--outdir&lt;/code&gt; selects where the PNG is saved.&lt;/p&gt;

&lt;p&gt;Code and README: &lt;a href="https://github.com/ratwolfzero/hopalong_phase_space/tree/main/Development" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>performance</category>
      <category>programming</category>
      <category>python</category>
    </item>
    <item>
      <title>The Hopalong "Attractor" That Isn't One</title>
      <dc:creator>Rat Wolf (ratwolf)</dc:creator>
      <pubDate>Sun, 25 Oct 2026 06:53:31 +0000</pubDate>
      <link>https://dev.to/ratwolf/the-hopalong-attractor-that-isnt-one-2mne</link>
      <guid>https://dev.to/ratwolf/the-hopalong-attractor-that-isnt-one-2mne</guid>
      <description>&lt;h2&gt;
  
  
  A classic computer-art map has no attractor. Here is how to see its regular and chaotic regions with a few lines of Python and numba
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5hhsgglq7b7twhak2sp8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5hhsgglq7b7twhak2sp8.png" alt="Header" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Orbits of the Hopalong map. Cool colours are regular orbits circling islands, warm colours are chaotic orbits.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Search for "Hopalong attractor" and you get glowing, symmetric pictures made from nothing but two lines of arithmetic. They are fun to produce and easy to share. But the word "attractor" hides what the picture actually is.&lt;/p&gt;

&lt;p&gt;This article walks through what the Hopalong map does, why the name is misleading, and how to see all of its phase space at once instead of one orbit at a time. None of this is new to dynamical-systems people. The point is that a short numba script makes it easy to see for yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The map
&lt;/h2&gt;

&lt;p&gt;Barry Martin's Hopalong map [2] turns each point of an orbit into the next one, using three parameters a, b and c:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbaer9rzqg97djon9rafv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbaer9rzqg97djon9rafv.png" alt="Map" width="798" height="163"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Start at (0,0), repeat millions of times, plot every point. With a = 0.7, b = 0.4 and c = 0 you get a picture like the one below. (In German the map appeared as the program HÜPFER in A.K. Dewdney's column in &lt;em&gt;Spektrum der Wissenschaft&lt;/em&gt; [3].)&lt;/p&gt;

&lt;h2&gt;
  
  
  One orbit, one picture
&lt;/h2&gt;

&lt;p&gt;The core loop is short. With numba it compiles to machine code and runs ten million iterations in about a second:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;numba&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;njit&lt;/span&gt;

&lt;span class="nd"&gt;@njit&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;orbit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
        &lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ys&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(This is a simplified version of the script's loop, which also discards a short transient and stops if an orbit escapes to infinity.)&lt;/p&gt;

&lt;p&gt;A scatter plot of ten million points is slow and saturates into a solid blob. A log-scaled 2D histogram is faster and shows structure:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9vfk7mygetdgv926kvwd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9vfk7mygetdgv926kvwd.png" alt="Orbit" width="800" height="815"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice the black regions. The orbit never enters them, and they are the first hint that something is going on.&lt;/p&gt;

&lt;p&gt;The picture also depends on how long you run the orbit. In one run it stayed inside the same box from 2 million up to 100 million points, then leaked into a larger region somewhere between 100 and 300 million points, and the box grew from about x −0.76 to 1.02 to about x −1.38 to 1.92. The exact point at which this happens depends on floating-point details, so treat it as an observation, not a constant. The script measures the extent over the full run so the image is framed correctly either way.&lt;/p&gt;

&lt;p&gt;For long runs the script fills the 2D histogram while the orbit runs instead of storing the points, which keeps memory constant (in one test, 300 million points took about 11 seconds and about 200 MB) and gave counts identical to NumPy's &lt;code&gt;histogram2d&lt;/code&gt; in the checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it is not an attractor
&lt;/h2&gt;

&lt;p&gt;An attractor is a set that nearby starting points are pulled toward. That requires the dynamics to shrink areas in phase space. The Hopalong map does not. Wherever it is differentiable, its Jacobian is&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkvj7fc7hbrl1ig5shz05.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkvj7fc7hbrl1ig5shz05.png" alt="Jacobian" width="800" height="164"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;and its determinant is&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7y9dp81gb4sjkgxf2drx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7y9dp81gb4sjkgxf2drx.png" alt="Determinant" width="796" height="83"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The map is also invertible: equation (2) gives the old x from the new y, and then equation (1) gives the old y. A map that is invertible and has determinant 1 preserves area. Nothing contracts, so there is nothing to attract.&lt;/p&gt;

&lt;p&gt;What you see in the classic picture is the footprint of one orbit, and a different starting point can give a different picture. The name is a loose label, and it is common to call any shape drawn by an iterated map an attractor. It is fine to keep using it, as long as you know what the image is.&lt;/p&gt;

&lt;p&gt;Two more facts about the map will matter later. It keeps its form under the scaling&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftxi8t0yclz6pu8q4fae8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftxi8t0yclz6pu8q4fae8.png" alt="Scaling" width="796" height="95"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;for any scale factor s, so a and b behave like lengths and c like a length squared. And its fixed points satisfy&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjyjzyyi94m8abnhrerf6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjyjzyyi94m8abnhrerf6.png" alt="Fixed Point" width="797" height="119"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;with y = a − x.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeing all of phase space
&lt;/h2&gt;

&lt;p&gt;If one orbit only tells part of the story, we want a way to classify every starting point. The standard tool is the Lyapunov exponent: how fast two nearby orbits separate.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;regular&lt;/strong&gt; orbit stays on a closed curve or a chain of islands. Nearby orbits separate slowly and the exponent is about zero.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;chaotic&lt;/strong&gt; orbit fills an area. Nearby orbits separate exponentially and the exponent is positive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To estimate it, a tangent vector is pushed forward by the Jacobian (3) and renormalised at every step; the hat marks a vector rescaled to length 1:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdfpdsloq64m0ttdd926x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdfpdsloq64m0ttdd926x.png" alt="Tangent" width="800" height="93"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs9by97l2mkwlwsx4xouj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs9by97l2mkwlwsx4xouj.png" alt="Renormalised" width="800" height="127"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The largest finite-time exponent over N steps is the mean logarithmic stretching:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9m5q1tflo42u835tcmrk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9m5q1tflo42u835tcmrk.png" alt="Lyapunov" width="799" height="157"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In code, it is the same loop with a few extra lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@njit&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lyapunov&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;vx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="mf"&gt;1e-12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# guard the sqrt singularity
&lt;/span&gt;        &lt;span class="n"&gt;dfdx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
        &lt;span class="n"&gt;vx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dfdx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;vx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;vy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;vx&lt;/span&gt;
        &lt;span class="n"&gt;norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;vx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;vy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;vy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;vx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vx&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vy&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;norm&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Also simplified: the script additionally discards the first 500 steps so the tangent vector can align, and stops if the orbit escapes.)&lt;/p&gt;

&lt;p&gt;Run it over a 400 × 400 grid of starting points (numba's &lt;code&gt;prange&lt;/code&gt; spreads the grid over your cores) and colour each point by its exponent:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvmj2or9fohz5xdbbs5fr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvmj2or9fohz5xdbbs5fr.png" alt="IExponent" width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the plotted window (x from −2.5 to 2.9, y from −2.2 to 3.2), about 70% of the starting points are regular and about 30% are chaotic, with 8,000 iterations per point. The dark blobs are islands of regular motion, and the bright bands between them are the chaotic region. The cross marks (0,0). Its orbit has an exponent of about 0.018 and lies in the chaotic region, which is why the classic picture has those empty holes: it is one chaotic orbit, and most of the empty holes are regular islands it does not enter (in a pixel-by-pixel check, about 81% of the empty pixels start regular orbits).&lt;/p&gt;

&lt;p&gt;Starting points far from the centre, such as (50,50) or (1000,−1000), turned out to be regular, but a coarse survey still found chaotic orbits at every distance we probed (up to 64 units from the origin), so the structure has no sharp outer edge.&lt;/p&gt;

&lt;h2&gt;
  
  
  A prettier version
&lt;/h2&gt;

&lt;p&gt;The classification map is informative but pixelated. For a more attractive picture, start a few hundred orbits along a line through phase space and draw each one as its own set of points. Regular orbits become nested rings around the islands, and chaotic ones fill the bands in between:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvp2pr0d7g6dn3uz0ygap.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvp2pr0d7g6dn3uz0ygap.png" alt="Portrait" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The warm colours (yellow to red) are chaotic orbits, brighter for a larger exponent. The cool colours (cyan to magenta) are regular orbits, but there they only encode the position of the starting point along the line, so they are decoration, not data.&lt;/p&gt;

&lt;p&gt;The picture shows a ring of seven large islands around the centre and an inner ring of about ten smaller ones. The line of starting points only grazes the rims of the large islands, so their interiors stay empty. We did not analyse these island chains further.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is, and what it isn't
&lt;/h2&gt;

&lt;p&gt;Mixed regular and chaotic motion is the standard behaviour of area-preserving maps, and this article reproduces it for one parameter set. The references at the end give the sources of the map; the results here have not been compared with them or with other literature on its dynamics. A few limits are worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The percentages depend on the window.&lt;/strong&gt; The chaotic share is about 31% in a window twice as wide as the one above, but about 20% at four times as wide. The numbers describe the plotted region, not the whole plane.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The threshold matters a little.&lt;/strong&gt; The chaotic share stays at 30–31% for thresholds from 0.002 to 0.004 and for 4,000 to 20,000 iterations. At a threshold of 0.008 it drops to about 25–27%, because weakly chaotic orbits are no longer counted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The exponent is finite-time.&lt;/strong&gt; Orbits that stick near an island can look regular over a short run, so the chaotic share can be under-counted near island boundaries. Even the exponent of the orbit from (0,0) varies by about 10% between runs of different length.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The square-root term is singular at b·x = c.&lt;/strong&gt; The derivative is unbounded there, so classical results for smooth maps are an analogy, not a guarantee.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One parameter set.&lt;/strong&gt; Nothing here says how the picture changes as a, b and c vary.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The full script produces all three views and the header image, with a, b, c and the iteration counts as command-line options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install numpy matplotlib numba
python hopalong_phase_space.py --save
python hopalong_phase_space.py --a 1.1 --b 0.5 --c 0.3 --iters 2000000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script runs in two passes. A quick first pass finds the length scale of the map from a, b and c, the centre of its structure (the map's fixed point) and the exact extent of the orbit from (0,0). The second pass draws the three views with that framing, so you can change a, b, c and the number of points and still get a framed picture. How much of the structure fits the window depends on the parameters, though: the window is 3.8 length scales around the fixed point (or around the middle of the orbit if there is none), which is a zoom choice, and for maps with much larger orbits you will want &lt;code&gt;--window-scale&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Code and README: &lt;a href="https://github.com/ratwolfzero/hopalong_phase_space" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A pretty picture is a good way into a real question. Here the question is how a map that preserves area still produces such rich structure, and the answer is that its phase space is not one thing but a patchwork of regular and chaotic regions. The natural next step is to ask how that patchwork changes as the parameters move.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;[1] &lt;strong&gt;J. Lansdown and R. A. Earnshaw (eds.)&lt;/strong&gt;, &lt;em&gt;Computers in Art, Design and Animation&lt;/em&gt;. New York: Springer-Verlag, 1989.&lt;/p&gt;

&lt;p&gt;[2] &lt;strong&gt;Barry Martin&lt;/strong&gt;, "Graphic Potential of Recursive Functions," in &lt;em&gt;Computers in Art, Design and Animation&lt;/em&gt; [1], pp. 109–129.&lt;/p&gt;

&lt;p&gt;[3] &lt;strong&gt;A.K. Dewdney&lt;/strong&gt;, &lt;em&gt;Psychotapeten&lt;/em&gt;, algorithm &lt;em&gt;HÜPFER&lt;/em&gt;, in &lt;em&gt;Spektrum der Wissenschaft: Computer Kurzweil&lt;/em&gt;. Heidelberg: Spektrum der Wissenschaft Verlagsgesellschaft mbH &amp;amp; Co., 1988. (German edition of &lt;em&gt;Scientific American&lt;/em&gt;.)&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>python</category>
      <category>performance</category>
      <category>programming</category>
    </item>
    <item>
      <title>Discreteness as the Late-Time Limit of a Smooth Flow</title>
      <dc:creator>Rat Wolf (ratwolf)</dc:creator>
      <pubDate>Fri, 09 Oct 2026 03:55:32 +0000</pubDate>
      <link>https://dev.to/ratwolf/discreteness-as-the-late-time-limit-of-a-smooth-flow-3ia4</link>
      <guid>https://dev.to/ratwolf/discreteness-as-the-late-time-limit-of-a-smooth-flow-3ia4</guid>
      <description>&lt;p&gt;&lt;em&gt;An exactly solvable gradient flow whose time-evolution maps are smooth bijections of the real line at every finite time, and whose infinite-time limit is rounding to the nearest integer.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxytu1d5trima4lf5apo4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxytu1d5trima4lf5apo4.png" alt="Dashboard" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Abstract
&lt;/h3&gt;

&lt;p&gt;We study the gradient flow of the periodic potential&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgufwkj09mcaox2piqaa8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgufwkj09mcaox2piqaa8.png" alt="Potential" width="269" height="79"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Integers are its stable fixed points and half-integers its unstable ones. The flow can be solved in closed form. At every finite time it is a smooth, strictly increasing bijection of the real line; the family of maps forms a one-parameter group; and as time tends to infinity every open cell between consecutive half-integers collapses onto the integer it contains. "Integer-valued" therefore appears as the endpoint of a continuous process with a single, exactly composable resolution parameter, the time. The integers are not generated by the flow: they enter through the period of the potential. What the flow provides is an exactly solvable, exactly composable route to that prescribed discrete target. We state the closed form, derive the main properties, give a geometric interpretation as a hyperbolic Möbius dilation of the circle, and give the exact time it takes to resolve a point to a given accuracy.&lt;/p&gt;

&lt;h3&gt;
  
  
  The exact solution
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frokh8ik3j922u1g48c2p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frokh8ik3j922u1g48c2p.png" alt="Exact" width="416" height="80"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The full derivations and proofs, the verification script and the visualization code are in the GitHub repo: &lt;a href="https://github.com/ratwolfzero/continuous-discreteness" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>computerscience</category>
      <category>science</category>
    </item>
    <item>
      <title>I Built a Finite-Element Solver With Nothing to Hide</title>
      <dc:creator>Rat Wolf (ratwolf)</dc:creator>
      <pubDate>Thu, 16 Jul 2026 01:13:07 +0000</pubDate>
      <link>https://dev.to/ratwolf/i-built-a-finite-element-solver-with-nothing-to-hide-4bm4</link>
      <guid>https://dev.to/ratwolf/i-built-a-finite-element-solver-with-nothing-to-hide-4bm4</guid>
      <description>&lt;p&gt;&lt;em&gt;A transparent, dependency-free capacitor simulator — and an honest account of exactly where a simple mesh lets it down.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4scfm7eo2bauklaydajc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4scfm7eo2bauklaydajc.png" alt="Plate Capacitor" width="800" height="677"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every physics course teaches the parallel-plate capacitor formula in the first week:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc160cqt92lscijj0gl9c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc160cqt92lscijj0gl9c.png" alt="Plate Capacitor" width="332" height="130"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clean. Memorable. &lt;strong&gt;Wrong&lt;/strong&gt; the moment your capacitor isn't two infinite plates in a vacuum. Real capacitors have edges. Real dielectrics are layered. Real geometries are curved. The formula survives exactly as long as the idealization does.&lt;/p&gt;

&lt;p&gt;So you reach for simulation instead — and run into two unsatisfying options.&lt;/p&gt;

&lt;p&gt;Option one: a commercial FEM package. Powerful, polished, and a complete black box. You get a number. You don't get to see the mesh, the matrix, or the fifty implicit decisions that produced it.&lt;/p&gt;

&lt;p&gt;Option two: a serious open-source stack — FEniCS, deal.II, scikit-fem with gmsh underneath. Genuinely open, genuinely capable, and also genuinely heavy: compiled mesh generators, several abstraction layers, a real learning curve before you can even see where the physics happens.&lt;/p&gt;

&lt;p&gt;I wanted a third thing. So I set myself three rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every formula in the code has to be traceable to something you could write on a whiteboard.&lt;/li&gt;
&lt;li&gt;Every accuracy claim has to be measured, not assumed.&lt;/li&gt;
&lt;li&gt;Every known limitation has to be printed by the program itself, not buried in a comment nobody reads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what building to those rules actually looked like.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Physics, Compressed
&lt;/h2&gt;

&lt;p&gt;A capacitor problem is one equation. Given some conductors at fixed voltages and a dielectric — possibly several, possibly varying in space — filling the gaps between them, the electric potential $V$ satisfies:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzavnqwro2tuouefqrwdo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzavnqwro2tuouefqrwdo.png" alt="Potential" width="350" height="72"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the whole physics. Everything else — field, displacement, stored energy, capacitance — falls out of &lt;em&gt;&lt;strong&gt;V&lt;/strong&gt;&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh9a48glr3h0dil32dovj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh9a48glr3h0dil32dovj.png" alt="Physics Details" width="775" height="89"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That last formula is worth a pause. The obvious way to get capacitance out of a simulation is to integrate surface charge on a conductor. This solver doesn't do that — it computes the &lt;em&gt;total energy stored in the field&lt;/em&gt; instead and rearranges that same relationship to solve for C. Charge integration means differentiating a numerically noisy field exactly at a boundary, the single worst place to differentiate anything. Energy integration only needs a field the solver has already computed everywhere. Small choice, but the kind that separates code that runs from code that's numerically honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "No Black Box" Actually Means
&lt;/h2&gt;

&lt;p&gt;Every finite element method runs on the same trick: chop the domain into small pieces, assume the solution is simple on each piece, turn a differential equation into a large sparse system of linear equations.&lt;/p&gt;

&lt;p&gt;For a triangular piece, the entire physics collapses into one formula for how strongly two corners of that triangle interact:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftk6m5iksw3qpkwsdadnn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftk6m5iksw3qpkwsdadnn.png" alt="Grid" width="268" height="84"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;𝐴 is the triangle’s area. 𝑏𝑖 and 𝑐𝑖 aren’t mysterious — they’re just numbers describing the edge directly across from corner 𝑖: bigger for a longer opposite edge, and they flip sign depending on which way that edge points. Feed in three corner coordinates and a material, and out comes a small table of these numbers, one for every pair of corners. That’s the whole trick. There’s no hidden layer between this formula and the code that computes it — about twenty lines of NumPy, every one of them readable.&lt;/p&gt;

&lt;p&gt;Assemble that over every triangle, apply boundary conditions — a conductor is just a region held at a fixed voltage — solve the sparse system, and you have 𝑉 everywhere. The same twenty lines handle a flat plate, a circular cable cross-section, or any shape you can build from simple ones combined with union, intersection, and difference. The geometry never touches the physics.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Deliberate Trade-Off
&lt;/h2&gt;

&lt;p&gt;Here's the part most "look what I built" posts skip: an honest accounting of what it costs.&lt;/p&gt;

&lt;p&gt;Real finite element tools mesh the &lt;em&gt;actual&lt;/em&gt; geometry — triangles that hug a curved boundary exactly, dense where the field is doing something interesting, sparse where it isn't. Building that kind of mesh is genuinely hard, and it's exactly what pulls in the heavy dependencies — a geometry kernel, mesh-quality algorithms, compiled binaries — that make FEM tooling opaque in the first place.&lt;/p&gt;

&lt;p&gt;I made a different choice: a plain Cartesian grid, split into triangles. No mesh generator. No native dependencies — just &lt;code&gt;numpy.linspace&lt;/code&gt; and index arithmetic. A circle on a grid like this isn't a circle. It's a staircase of grid cells that looks roughly circular from far enough away.&lt;/p&gt;

&lt;p&gt;That's not an oversight. It's the trade, made on purpose, to keep the whole tool inside three lines of &lt;code&gt;pip install&lt;/code&gt;. The question is whether the trade is &lt;em&gt;honest&lt;/em&gt; about its cost — and that's where most projects stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving It's Not a Toy
&lt;/h2&gt;

&lt;p&gt;Before trusting any result, I checked the solver against a case with a known &lt;em&gt;exact&lt;/em&gt; answer: plates spanning the entire simulation width, so no fringing is even geometrically possible. Just,&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvqhqcnljpdomo1agoybo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvqhqcnljpdomo1agoybo.png" alt="Capacity" width="165" height="48"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;exactly, with nothing to approximate on the physics side.&lt;/p&gt;

&lt;p&gt;The solver matched it to &lt;strong&gt;0.0000% error, at every mesh resolution tested.&lt;/strong&gt; That's the baseline: the finite element machinery has no bugs hiding in it. Whatever error shows up elsewhere comes from the mesh, not the math.&lt;/p&gt;

&lt;p&gt;That check used a deliberately artificial setup — plates spanning the whole domain so fringing couldn’t happen — specifically so the formula would be exact and the test would be clean.&lt;br&gt;
Real capacitors don’t look like that. So next I ran the two real examples, with real, finite edges — a coax cable filled with polyethylene, and a parallel plate with a glass slab across half the gap — through an actual convergence study instead of reporting one number and hoping.&lt;br&gt;
The two reference values below aren’t the same kind of thing, on purpose: the coax formula is exact, so its gap is genuine mesh error. The parallel-plate formula, by contrast, is deliberately fringing-free — the classic no-fringing approximation, now extended to a glass-and-air sandwich instead of a single material — so its gap here isn’t a solver problem, it’s the real physics that formula was never built to capture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyblfb4ka7lw6jxr6ikn3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyblfb4ka7lw6jxr6ikn3.png" alt="Convergence" width="800" height="108"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The coax case increases at every step, though not at a perfectly steady rate — there’s a plateau partway through, still climbing but less steeply.&lt;/p&gt;

&lt;p&gt;The parallel-plate case looks different again — across the four resolutions actually tested, it runs 102.0 → 101.9 → 98.8 → 101.8 pF/m, dipping in the middle before recovering, rather than settling smoothly toward the table’s two endpoints.&lt;/p&gt;

&lt;p&gt;Two things are tangled together in that dip.&lt;br&gt;
&lt;strong&gt;One is mundane&lt;/strong&gt;: a fixed grid can only land exactly on some target sizes, and this particular resolution happens to round the capacitor’s own gap and dielectric thickness to slightly different values than its neighbors do — a small, real change in what’s actually being simulated, not an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The other is more interesting&lt;/strong&gt;: a conductor’s sharp corner carries a genuine field singularity ( (𝐸∼𝑟^−1/3, for anyone curious), and independent meshes built at different resolutions aren’t strict refinements of each other, so the usual “finer mesh, better answer” guarantee doesn’t hold between them.&lt;/p&gt;

&lt;p&gt;I could have shown just the two endpoints and let it look like ordinary convergence. Instead the program prints the whole sweep, and flags which of the two effects is at play at each point, rather than leaving the reader to guess.&lt;/p&gt;

&lt;p&gt;A tool that only shows you its clean case isn’t more trustworthy than one that shows you both. It’s just quieter about its limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Is — and What It Isn't
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;This is&lt;/th&gt;
&lt;th&gt;This is not&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A from-scratch, mathematically correct FEM electrostatics solver&lt;/td&gt;
&lt;td&gt;A replacement for COMSOL, Ansys Maxwell, or a production EDA tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small enough to read in one sitting — about 1,000 lines, three dependencies&lt;/td&gt;
&lt;td&gt;An adaptive, conforming-mesh solver — curved boundaries are approximated, and quantifiably so&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validated against exact solutions, not just "it runs and the number looks plausible"&lt;/td&gt;
&lt;td&gt;A 3D solver, or one that ingests arbitrary CAD geometry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Honest, in its own printed output, about exactly where and why it's approximate&lt;/td&gt;
&lt;td&gt;Trying to be any of the above&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you need production-grade accuracy on complex geometry, this isn't your tool. If you want to actually &lt;strong&gt;&lt;em&gt;see&lt;/em&gt; what a finite element solver is doing&lt;/strong&gt; — every matrix entry, every approximation, every place it's wrong and by how much — &lt;strong&gt;this was built for exactly that.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Goes From Here
&lt;/h2&gt;

&lt;p&gt;The single biggest lever left on the table is the mesh itself. A few concrete next steps, in roughly the order they'd pay off:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Swap the Cartesian grid for a real unstructured, conforming mesh, fixing the staircased boundaries and under-resolved corners in one move — at the cost of the dependency-free simplicity that's the whole point of this version.&lt;/li&gt;
&lt;li&gt;Short of that: grade the existing grid finer near sharp corners while staying dependency-free, which would tighten the parallel-plate numbers specifically without touching the coax case.&lt;/li&gt;
&lt;li&gt;Add the classic extras once the mesh is solid: floating conductors, nonlinear and anisotropic dielectrics, 3D.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that happens silently. That's kind of the point.&lt;/p&gt;

&lt;p&gt;The full code, the complete derivation, and both worked examples with figures are on: &lt;a href="https://github.com/ratwolfzero/Capacitor_FEM/tree/main" rel="noopener noreferrer"&gt;&lt;em&gt;GitHub&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>fem</category>
      <category>capacitor</category>
      <category>electrostatics</category>
      <category>python</category>
    </item>
    <item>
      <title>A Measured Formulation of the Shadow View of Collatz</title>
      <dc:creator>Rat Wolf (ratwolf)</dc:creator>
      <pubDate>Fri, 03 Jul 2026 06:53:02 +0000</pubDate>
      <link>https://dev.to/ratwolf/a-measured-formulation-of-the-shadow-view-of-collatz-2l5</link>
      <guid>https://dev.to/ratwolf/a-measured-formulation-of-the-shadow-view-of-collatz-2l5</guid>
      <description>&lt;h2&gt;
  
  
  A Measured Formulation of the Shadow View of Collatz
&lt;/h2&gt;

&lt;h2&gt;
  
  
  A more careful description
&lt;/h2&gt;

&lt;p&gt;A useful way to think about the Collatz map is to separate the orbit into two layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the parity pattern of the terms;&lt;/li&gt;
&lt;li&gt;the arithmetic values that carry that pattern.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If one records the orbit as a sequence of even/odd decisions, the resulting binary word can be viewed as a symbolic encoding of the dynamics. In the accelerated version of the map, where one removes all factors of 2 immediately after each odd step, the evolution of the values becomes closely tied to this parity word. At that symbolic level, the dynamics look simple: the orbit is encoded by a binary sequence, and the next state is determined by the next bit of that sequence.&lt;/p&gt;

&lt;p&gt;This is a genuine simplification of the picture, but it is a simplification of the representation, not a solution of the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Accelerated Collatz Formulation
&lt;/h2&gt;

&lt;p&gt;This perspective uses the &lt;em&gt;accelerated&lt;/em&gt; (or shortcut) Collatz map rather than the classical 3n+1 rule. In the accelerated version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If &lt;strong&gt;n is even&lt;/strong&gt;: next = n / 2&lt;/li&gt;
&lt;li&gt;If &lt;strong&gt;n is odd&lt;/strong&gt;:  next = (3n + 1) / 2&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By folding the immediate division by 2 after each 3n+1 step into a single operation, the accelerated map produces a shorter sequence in which every step corresponds directly to one bit in the parity word (0 = divide by 2, 1 = apply (3n+1)/2). For example, starting from 27 the classical orbit requires 111 steps, while the accelerated orbit reaches 1 in 71 steps. This formulation makes the parity shadow cleaner and more tightly coupled to the symbolic dynamics, while preserving the essential behavior of the original conjecture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6msbsfh7b66j01twrz0f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6msbsfh7b66j01twrz0f.png" alt="Block Recurrence Metric" width="800" height="586"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Collatz Shadow Explorer
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk943cuzuyk8imnylbozp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk943cuzuyk8imnylbozp.png" alt="Shadow Explorer" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To actively investigate these dynamics, this repository includes a Python-based interactive desktop application (&lt;code&gt;shadow_explorer.py&lt;/code&gt;). &lt;a href="https://github.com/ratwolfzero/Collatz/blob/main/shadow_approach/shadow_explorer.py" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rather than viewing static plots, the application allows you to input starting values, adjust sequence parameters, and dynamically generate up to four concurrent analytical views of the parity shadow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Analytical Views Included
&lt;/h3&gt;

&lt;p&gt;The application provides several lenses through which to study the sequence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Orbit + Shadow Track:&lt;/strong&gt; The fundamental view. It overlays the numeric orbit (the arithmetic layer) with the binary parity sequence (the symbolic layer) on the same step axis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rolling Density Analysis:&lt;/strong&gt; Displays the ratio of odd to even steps over a sliding window, highlighting localized phases of growth or decay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;:&lt;/strong&gt; A histogram showing the frequency of $k$-length binary blocks (e.g., how often &lt;code&gt;011&lt;/code&gt; or &lt;code&gt;101&lt;/code&gt; occurs), revealing combinatorial constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2D Turtle Walk (Fractal):&lt;/strong&gt; Treats the parity word as directional instructions (0 = Left, 1 = Right) on a 2D plane. This maps the sequence to spatial geometry, often revealing structural symmetries hidden in the linear data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shannon Block Entropy:&lt;/strong&gt; Measures the informational unpredictability of the sequence over time. It highlights chaotic phases versus highly ordered phases as the orbit collapses toward 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast Fourier Transform (FFT):&lt;/strong&gt; Calculates the power spectrum of the $0-1$ signal, allowing you to check for dominant frequencies or periodic harmonics in the shadow word.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;De Bruijn Transition Network:&lt;/strong&gt; A directed network graph of parity block transitions. It visualizes the chronological "flow" of the sequence, essentially mapping the pathways that drain into the standard 4-2-1 attractor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;more see development folder on GitHub:&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this perspective is
&lt;/h2&gt;

&lt;p&gt;This viewpoint is best understood as a reformulation. It highlights that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the Collatz map can be studied through its parity pattern;&lt;/li&gt;
&lt;li&gt;the parity pattern gives a symbolic description of the orbit;&lt;/li&gt;
&lt;li&gt;the arithmetic part of the map is pushed into the encoding of the symbolic sequence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this sense, the problem can be seen as a question about which binary sequences arise from ordinary integers under this encoding, and how those sequences are constrained by arithmetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this perspective is not
&lt;/h2&gt;

&lt;p&gt;It is important not to overstate what this does. It does not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;solve the Collatz conjecture;&lt;/li&gt;
&lt;li&gt;prove convergence for all starting values;&lt;/li&gt;
&lt;li&gt;replace the classical numerical and probabilistic analysis;&lt;/li&gt;
&lt;li&gt;introduce a fundamentally new dynamical system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main point is not that the problem becomes easy in this language. The point is that the structure becomes more legible: the symbolic part is simple, while the arithmetic constraints are the part that remains difficult.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it can still be useful
&lt;/h2&gt;

&lt;p&gt;Even if it does not solve the conjecture, this perspective can be valuable for at least three reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it gives a compact symbolic representation of the orbit;&lt;/li&gt;
&lt;li&gt;it separates the simple combinatorial aspect from the arithmetic constraint;&lt;/li&gt;
&lt;li&gt;it connects the problem with broader ideas from symbolic dynamics, signal processing, and 2-adic analysis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the idea is not that one has found a new mechanism for proving convergence. It is rather that one has found a different coordinate system—aided by computational visualization—in which the problem looks more structured and more interpretable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation and Usage
&lt;/h2&gt;

&lt;p&gt;To run the interactive explorer, you will need Python 3 installed along with a few standard scientific and GUI libraries.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
bash

# Install required dependencies
pip install PyQt6 matplotlib numpy networkx

# Launch the explorer
python shadow_explorer.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>mathematics</category>
      <category>collatz</category>
    </item>
    <item>
      <title>A Triplet-Based Parameterization for the Local Asymptotic Characterization of Polynomial Roots</title>
      <dc:creator>Rat Wolf (ratwolf)</dc:creator>
      <pubDate>Tue, 31 Mar 2026 08:24:01 +0000</pubDate>
      <link>https://dev.to/ratwolf/a-triplet-based-parameterization-for-the-local-asymptotic-characterization-of-polynomial-roots-33oa</link>
      <guid>https://dev.to/ratwolf/a-triplet-based-parameterization-for-the-local-asymptotic-characterization-of-polynomial-roots-33oa</guid>
      <description>&lt;p&gt;Abstract​&lt;/p&gt;

&lt;p&gt;This paper introduces a compact three-parameter framework for characterizing the local geometry of polynomial roots. For each root, the framework records its position, its algebraic multiplicity, and a newly defined quantity called the characteristic deflection distance &lt;strong&gt;𝛿&lt;/strong&gt;. This third parameter acts as a natural geometric scale: it measures how sharply or gradually the polynomial departs from zero in the immediate vicinity of the root, and it encodes the collective influence of all other roots through their distances from the one being analyzed.&lt;/p&gt;

&lt;p&gt;The characteristic deflection distance generalizes the classical condition number of a simple root to roots of arbitrary multiplicity, and it allows direct geometric comparison across roots of different degrees. A key finding is that multiplicity alone does not determine geometric dominance — a lower-multiplicity root can have a larger spatial footprint than a higher-multiplicity one, depending on the global root configuration.&lt;/p&gt;

&lt;p&gt;Symbolic and numerical Python implementations are provided, along with a worked example. The framework extends naturally to polynomials over the complex numbers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://doi.org/10.5281/zenodo.19303726" rel="noopener noreferrer"&gt;DOI (Zenodo)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To visualize the spatial competition between these triplets, we map the &lt;strong&gt;𝛿-Normalized Distance Field&lt;/strong&gt; and the &lt;strong&gt;Newton Flow&lt;/strong&gt;. This provides a "topographical" view of the polynomial's geometry where every root is evaluated against its own intrinsic scale.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ratwolfzero/Param_Poly_Root" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyx4hlrwxa8n4ky0khsuf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyx4hlrwxa8n4ky0khsuf.png" alt=" " width="800" height="720"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>computerscience</category>
      <category>science</category>
    </item>
    <item>
      <title>High-Precision Companion Matrix Root Finder</title>
      <dc:creator>Rat Wolf (ratwolf)</dc:creator>
      <pubDate>Fri, 13 Mar 2026 06:53:50 +0000</pubDate>
      <link>https://dev.to/ratwolf/high-precision-companion-matrix-root-finder-1ph7</link>
      <guid>https://dev.to/ratwolf/high-precision-companion-matrix-root-finder-1ph7</guid>
      <description>&lt;h2&gt;
  
  
  High-Precision Companion Matrix Root Finder
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/ratwolfzero/Poly_Root" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;br&gt;
&lt;a href="https://medium.com/@ratwolf/the-floating-point-catastrophe-9e795d46cfb1" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;High-Precision Companion Matrix Root Finder&lt;/p&gt;

&lt;p&gt;A robust polynomial solver leveraging arbitrary-precision floating-point arithmetic to compute roots of ill-conditioned polynomials. This tool primarily addresses rounding and finite-precision issues present in standard 64-bit floating-point implementations by constructing and solving a companion matrix within the mpmath environment.&lt;/p&gt;

&lt;p&gt;The solver cannot eliminate the intrinsic mathematical ill-conditioning of certain polynomials, but it can significantly reduce the numerical errors caused by rounding, making the computed roots far more reliable.&lt;/p&gt;

&lt;p&gt;It features modular code structure, residual checks, and robust input validation to improve usability and reliability.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Idea
&lt;/h2&gt;

&lt;p&gt;The solver does not attempt to solve the fundamental conditioning problem of polynomial root finding.&lt;/p&gt;

&lt;p&gt;Instead, it focuses on reducing numerical rounding errors using high‑precision arithmetic, which in practice leads to far more reliable root approximations for difficult polynomials.&lt;/p&gt;

</description>
      <category>python</category>
      <category>performance</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>Fast Chladni Pattern Simulation with a Simplified Membrane Model Using Vectorization and Stochastic Particles View</title>
      <dc:creator>Rat Wolf (ratwolf)</dc:creator>
      <pubDate>Thu, 05 Mar 2026 06:25:48 +0000</pubDate>
      <link>https://dev.to/ratwolf/fast-chladni-pattern-simulation-with-a-simplified-membrane-model-using-vectorization-and-stochastic-30gf</link>
      <guid>https://dev.to/ratwolf/fast-chladni-pattern-simulation-with-a-simplified-membrane-model-using-vectorization-and-stochastic-30gf</guid>
      <description>&lt;h2&gt;
  
  
  Chladni Figures Simulation
&lt;/h2&gt;

&lt;p&gt;This Python code simulates the nodal line patterns of Chladni figures, approximating Ernst Chladni’s 1787 experiments with vibrating plates by modeling the resonant modes of a square membrane. It captures the visual essence of these wave patterns without simulating the complex physics of elastic bending and the dynamics of particle motion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python Code, detailed simulation description, physics and mathematics see:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ratwolfzero/Chladni_Figures/tree/main" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F61z432duledtl75i2mju.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F61z432duledtl75i2mju.png" alt="Magnitude View" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Magnitude View:&lt;/strong&gt; A 2D static plot showing the time-independent displacement magnitude ∣Z(x,y,f)∣ of the steady-state response at a driving frequency f matching the degenerate modes (3,5) and (5,3). A small damping factor (γ = 0.01) ensures these modes dominate. The magnitude highlights nodal lines and makes positive and negative antinodes indistinguishable, mimicking experimental Chladni patterns.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr9vrk3dlfh8lo9ok0qem.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr9vrk3dlfh8lo9ok0qem.png" alt="Phase View" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase View:&lt;/strong&gt; This view displays the signed spatial amplitude of the steady-state response at a given frequency. The sign indicates whether a region oscillates in phase or π out of phase with a reference. Nodal lines appear where the amplitude vanishes. The membrane motion remains oscillatory in time; this view represents the spatial structure and phase relationships of that motion.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvuue6narguh138gvvtr1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvuue6narguh138gvvtr1.png" alt="Sand View" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sand View&lt;/strong&gt;: The Sand View acts as a visually intuitive “nodal detector” derived from the same resonance-weighted field used in the magnitude and phase views.&lt;/p&gt;

&lt;p&gt;Relation to Physical Experiments&lt;/p&gt;

&lt;p&gt;In real Chladni experiments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Grains bounce dynamically due to plate acceleration.
Migration occurs through complex frictional and collisional processes.&lt;/li&gt;
&lt;li&gt;Accumulation is influenced by time evolution and nonlinear interactions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Sand View abstracts these dynamics into a stationary probability model. It captures the final accumulation pattern without modeling transient motion.&lt;/p&gt;




&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Field-based simulation; does not model particle dynamics.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: This simulation visualizes the nodal patterns of a unit square membrane L_x = L_y with fixed edges, governed by the wave equation, capturing the essence of Chladni figures. In real experiments, elastic plates follow complex bending physics, but the membrane model approximates their nodal patterns using simpler wave mechanics. Focusing on nodal patterns avoids the unresolved complexity of grain motion, highlighting the universal physics of resonance, independent of material properties.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Only finite max_mode included.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Damping gamma is uniform; real plates have non-uniform damping.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Geometry is idealized unit square membrane.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Uses a membrane model for computational efficiency, which simplifies the physics of a true plate with bending stiffness.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Simulation–Experiment Match Fidelity
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode Category&lt;/th&gt;
&lt;th&gt;Typical Example(s)&lt;/th&gt;
&lt;th&gt;Match Fidelity&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Exact Symmetric Modes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(2,2), (3,3), (4,4)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Perfect match: Clean axial nodal lines align exactly with experiments.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Low-order Degenerate Pairs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(1,2)+(2,1), (1,3)+(3,1)&lt;/td&gt;
&lt;td&gt;~50–60%&lt;/td&gt;
&lt;td&gt;Partial match: Shows tilted/blended nodals instead of sharp crosses seen in some low-order plate patterns.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;High-order Degenerate Pairs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(3,5)+(5,3), (1,6)+(6,1)&lt;/td&gt;
&lt;td&gt;~90–95%&lt;/td&gt;
&lt;td&gt;Excellent match: Complex, interior nodals closely mimic historical figures; diagonals may flip orientation.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Notes:&lt;/strong&gt;  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Symmetric modes m,m are exact (100%) and visually identical to experiments.
&lt;/li&gt;
&lt;li&gt;Asymmetric modes m ≠ n always form degenerate pairs in this square model, blending into symmetric patterns—both contribute fully e.g., (1,2) and (2,1) dominate together. No isolated "single" asymmetric modes appear due to symmetry.
&lt;/li&gt;
&lt;li&gt;Low-order pairs can look less crisp; high-order excel at reproducing rich, experimental-like topology, including accidental degeneracies where symmetric modes blend with pairs (e.g., (10,10) + (2,14)/(14,2)). Larger gamma adds asymmetry from nearby modes, enhancing realism.
&lt;/li&gt;
&lt;li&gt;Ratings prioritize &lt;strong&gt;nodal patterns&lt;/strong&gt; (positions and shapes); the sim's blending captures the visual spirit of Chladni figures effectively, even if idealized.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Historical Context
&lt;/h2&gt;

&lt;p&gt;Ernst Chladni (1756–1827), known as the father of acoustics, pioneered the study of vibrational modes by observing how elastic plates, sprinkled with sand or powder, formed Chladni figures—geometric patterns along nodal lines where the surface remains stationary. These patterns reveal standing waves and remain a subject of research today.&lt;/p&gt;

&lt;p&gt;In his 1787 experiments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;An elastic plate (thin brass plate) was fixed at its center or edges.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It was vibrated with a violin bow at various frequencies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Particles migrated from high-vibration areas to nodal lines, creating striking patterns.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Chladni figures laid foundations for acoustics, wave physics, and applications like mechanical engineering, with their nodal patterns inspiring both historical and modern scientific inquiry.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Chladni, Ernst Florens Friedrich. &lt;em&gt;Entdeckungen über die Theorie des Klanges&lt;/em&gt;. Leipzig: Weidmanns Erben und Reich, 1787. ETH-Bibliothek Zürich,&lt;br&gt;
Rar 5284. &lt;a href="https://doi.org/10.3931/e-rara-4235" rel="noopener noreferrer"&gt;https://doi.org/10.3931/e-rara-4235&lt;/a&gt;. Public Domain Mark.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Herman, Russell. Vibrations of Rectangular Membranes. (2024, September 4) University of North Carolina Wilmington. CC BY‑NC‑SA. &lt;a href="https://math.libretexts.org/@go/page/90264" rel="noopener noreferrer"&gt;https://math.libretexts.org/@go/page/90264&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wikipedia contributors. “Ernst Chladni.” Wikipedia. &lt;a href="https://en.wikipedia.org/wiki/Ernst_Chladni" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/Ernst_Chladni&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Further Reading&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Abramian, A., Protière, S., Lazarus, A., &amp;amp; Devauchelle, O.&lt;br&gt;
Chladni patterns explained by the space-dependent diffusion of bouncing grains.&lt;br&gt;
Phys. Rev. Research 7, L032001 — Published 1 July,2025&lt;br&gt;
&lt;a href="https://doi.org/10.1103/PhysRevResearch.7.L032001" rel="noopener noreferrer"&gt;https://doi.org/10.1103/PhysRevResearch.7.L032001&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tuan, P.H., Lai, Y.H., Wen, C.P. et al. Point-driven modern Chladni figures with symmetry breaking. Sci Rep 8, 10844 (2018).&lt;br&gt;
&lt;a href="https://doi.org/10.1038/s41598-018-29244-6" rel="noopener noreferrer"&gt;https://doi.org/10.1038/s41598-018-29244-6&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tseng, Yu-Chen, Yu-Hsin Hsu, Yu-Hsiang Lai, Yan-Ting Yu, Hsing-Chih Liang, Kai-Feng Huang, and Yung-Fu Chen. 2021. “Exploiting Modern Chladni Plates to Analogously Manifest the Point Interaction” Applied Sciences 11, no. 21: 10094. &lt;a href="https://doi.org/10.3390/app112110094" rel="noopener noreferrer"&gt;https://doi.org/10.3390/app112110094&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Becker, R. (2025, September 12). Unveiling the hidden geometry of sound. Medium. &lt;a href="https://medium.com/@ratwolf/unveiling-the-hidden-geometry-of-sound-c3f0cf09dba5" rel="noopener noreferrer"&gt;https://medium.com/@ratwolf/unveiling-the-hidden-geometry-of-sound-c3f0cf09dba5&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>softwaredevelopment</category>
      <category>computerscience</category>
      <category>science</category>
    </item>
    <item>
      <title>An old Classic with new Perspective in 2D and 3D</title>
      <dc:creator>Rat Wolf (ratwolf)</dc:creator>
      <pubDate>Wed, 04 Dec 2024 14:31:34 +0000</pubDate>
      <link>https://dev.to/ratwolf/an-old-classic-with-new-perspective-in-3d-48af</link>
      <guid>https://dev.to/ratwolf/an-old-classic-with-new-perspective-in-3d-48af</guid>
      <description>&lt;h2&gt;
  
  
  Historical Context
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Hopalong&lt;/strong&gt;* attractor, authored by Barry Martin of Aston University in Birmingham, England [2], was popularized by A.K. Dewdney in the September 1986 issue of &lt;em&gt;Scientific American&lt;/em&gt;. In Germany, the &lt;em&gt;Hopalong&lt;/em&gt; attractor gained further popularity through an algorithm called &lt;em&gt;HÜPFER&lt;/em&gt; in the article &lt;em&gt;Psychotapeten&lt;/em&gt; published in &lt;em&gt;Spektrum der Wissenschaft&lt;/em&gt;. [3].&lt;br&gt;&lt;br&gt;
*Nicknamed by A.K. Dewdney.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python Code, detailed description and mathematics see:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ratwolfzero/hopalong_python" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hopalong Attractor Functions
&lt;/h2&gt;

&lt;p&gt;The mathematical definition of the Hopalong attractor is given by the following system of recursive functions, defined in [2]:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1hfkhthh8u4o1euej8m2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1hfkhthh8u4o1euej8m2.png" alt="Hopalong" width="301" height="63"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The sequence starts from the initial point (x0 , y0) = (0 , 0).&lt;/li&gt;
&lt;li&gt;xn and yn represent the coordinates at the n-th iteration of the attractor.&lt;/li&gt;
&lt;li&gt;a, b, and c are parameters influencing the attractor's dynamics.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;sgn&lt;/em&gt; is the &lt;em&gt;signum&lt;/em&gt; function. However, the programs use &lt;code&gt;math.copysign()&lt;/code&gt;, which is defined as follows:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqqk551mxeho18dgnydy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqqk551mxeho18dgnydy.png" alt="Copysign" width="606" height="70"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Computational Approach
&lt;/h2&gt;

&lt;p&gt;The available Python programs calculate and visualize the attractor by iterating the described function system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Visual Representation&lt;/strong&gt;:&lt;br&gt;
The attractor is rendered as a density heatmap, with color intensity reflecting areas of high or low point concentration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Efficiency&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large numbers of iterations are computed with low memory usage and high processing speed.&lt;/li&gt;
&lt;li&gt;Just-in-time (JIT) compilation is applied and supported by a low-complexity code structure.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Core Algorithm:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The algorithm consists of two main passes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;First Pass&lt;/strong&gt;: Determines the spatial extent of the attractor trajectory.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Second Pass&lt;/strong&gt;: Maps the sequentially generated trajectory points in continuous space directly to a discrete pixel grid, keeping track of the number of pixel hits used to generate the density heatmap matrix. &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feqg2pyibs93ftqrd6qjw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feqg2pyibs93ftqrd6qjw.png" alt="Hopalong Attractor 2D" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjvlb3jg35p8vo9tuypj8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjvlb3jg35p8vo9tuypj8.png" alt="Hopalong Attractor 3D" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;[1]&lt;/strong&gt; J. Lansdown and R. A. Earnshaw (eds.), &lt;em&gt;Computers in Art, Design and Animation&lt;/em&gt;. New York: Springer-Verlag, 1989.&lt;br&gt;
e-ISBN-13: 978-1-4612-4538-4.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[2]&lt;/strong&gt; Barry Martin, "Graphic Potential of Recursive Functions," in &lt;em&gt;Computers in Art, Design and Animation&lt;/em&gt; [1], pp. 109–129.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[3]&lt;/strong&gt; A.K. Dewdney, &lt;em&gt;Psychotapeten&lt;/em&gt;, algorithm &lt;em&gt;HÜPFER&lt;/em&gt;, in &lt;em&gt;Spektrum der Wissenschaft: Computer Kurzweil&lt;/em&gt;.&lt;br&gt;
Spektrum der Wissenschaft Verlagsgesellschaft mbH &amp;amp; Co., Heidelberg, 1988.&lt;br&gt;
(German version of &lt;em&gt;Scientific American&lt;/em&gt;).&lt;br&gt;
ISBN-10: 3922508502, ISBN-13: 978-3922508502.&lt;/p&gt;

</description>
      <category>python</category>
      <category>attractors</category>
      <category>hopalong</category>
      <category>computerscience</category>
    </item>
  </channel>
</rss>
