Author: Judah Wright

  • The Bhuddabrot

    The Bhuddabrot

    Authors Note: This is a follow-up post to my post about Playing With The Mandelbrot Set.

    The Mandelbrot set is the set of all points on the complex plane where the function, f(n+1) = n^2 + c, continues to infinity without the result exceeding 2. It is the set of all complex numbers, n, applied to this function, where both the real and imaginary parts stay constrained.

    The Bhuddabrot is the inverse. It is the set of all numbers where the function outlined above does escape the bounds of 2. The image is a representation of the complex plane, where each iteration of every point is plotted in sequence. 

    In the Mandelbrot calculation, each pixel represents the number of iterations for that position. In the Bhuddabrot each pixel is a hit-counter describing the number of times a trajectory calculation has hit that point regardless of its starting position. The brighter Bhuddabrot pixel, the more times it was “hit” by a Mandelbrot trajectory.

    The important distinction is, the Mandelbrot shows the contours of the shape, whereas the Bhuddabrot shows all the data points generated by completing the Mandelbrot calculation. They are simply different ways of displaying the same data.

    Computation Challenges

    The shape of the work is as follows: I needed to perform a Mandelbrot calculation many times. To be precise, I had to do this calculation three times for each pixel in the image. Why three times? Because to color the image, the calculation must be completed with slightly different parameters for each color: red, green, and blue.

    The images along the way here are unclear. It’s a bit like looking into a pond. The colors red, green, and blue each represent a different depth in the pond. Red is “shallow” (least number of iterations), and blue is “deep” (most iterations), in the middle is green. As you transition from one color to the next, more details become apparent, but you can’t quite see the bottom.

    Initially, I struggled with having some noise in the image. The problem was less apparent at lower resolutions, but at higher resolutions noise became increasingly problematic. I initially suspected the disturbance as being floating point errors. To resolve these issues, I needed to iterate on the code, meaning that I test the code, make changes to a new version, and repeat until I got something usable. 

    Unfortunately, a full-resolution render of the image took my laptop a smidge under 4-days. And that was before cranking the quality slider. Changing the code, then literally waiting for days to see what (if any) impact the change made would prevent the project from concluding anytime soon. Sounded like I needed a side-quest!

    The good news is, each calculation point is independent of the others. In theory it would be possible to break the work into one “work unit” per “input-pixel” and then map the resulting heat-map onto an image.

    Swoole

    The next stop on my optimization journey was Swoole, a tool that allows users to write concurrent code. Using my previous render (let’s say 4 days) as a benchmark, and assuming I was able to divide the work evenly among CPU cores, a c8g.12xlarge AWS EC2 instance with 48 cores would require 2 hours. This napkin maths out to about $4 per run. If I forget to turn the machine off the $2/hr charge is going to get spendy quick.

    Costs aside, I considered going slower on smaller instances. What would that look like?
    Well, I would need a “work dispenser” that could yield work units from a generator. I also needed each “worker” to send its results back via some sort of channel. The last step was creating an image out of a reduction of all the results.

    After a quick chat with Greg in the PHP Freelancer’s Guild Discord, we decided that because of the necessity of the shared data, Swoole wasn’t the best fit for the project. With Swoole, I must scale vertically and the type of work doesn’t fit well.

    Note: If you have an idea for how to solve this problem with Swoole, leave a comment!

    Queues And Workers

    What if, instead of having a shared work dispenser, I broke the script into a few distinct steps? I thought this might work, and the steps I decided on are presented below:

    1. Pre-calculate the work to be done and insert each work unit into a queue.
    2. Workers pick up the work from the queue and write the resulting trajectory coordinates into a shared database.
    3. Finally, the database is queried for all coordinates in the image and some post-processing is done.

    Let’s do some napkin math on what each simulation would cost.

    • 6x c8g.2xlarge instances for two hours = $3.828 per run
    • 1x db.t3.small MariaDB instance = $36.32 per month

    Ok, so the numbers don’t look great. One way or another the compute is going to cost about $4/run, but here I also have the added overhead of the database.

    Another drawback is, there will be a network round-trip request for each work unit. Millions of work units are a lot of round-trip requests.

    Trade Offs

    With the Swoole method I was limited by the number of cores on any given physical machine. With the Queue route I was limited by the network overhead to distribute the work. Both methods cost about the same (minus the database).

    The advantage of the queue worker method was that it was possible to scale elastically. In theory, I could set up an auto-scaling-group to hold a baseline (or even zero) number of workers, and set it to scale up to a bunch of workers to consume the workload.

    If I went the queue route, I could adjust how quickly an image is generated by tuning the maximum number of instances it is allowed to spin up. Each image would still have the same overall price tag, but I would have a control for how quickly it would take to test changes to the code.

    The Code

    I was calculating a trajectory for a mandelbrot calculation which moves through a series of complex points. If I exceed my maximum number of iterations, I set it to return “false” as this point is inside the Mandelbrot set. Otherwise, the calculation would keep appending points within the bounds of the set to my trajectory until it found a point outside the set.

    The code is below:

    Noisy Image

    The image generated still had noise in it. And it wasn’t as pretty as the images found on Wikipedia, for example. That simply would not do. The project was supposed to be print-quality and “noise” is not print quality.

    Since I suspected the noise was coming from floating point errors, I spent some time employing my library (link here) rewriting everything to use my implementation instead. The gist of the change was that instead of working with approximately 14 decimal digits, I switched to working with 64 decimal digits and rounding off the remainder. I called the 64 decimal digit version “fancy math” and was certain that nothing bad would happen by employing it. I was wrong, but more on that later. 

    The initial results were promising. Here are side-by-side 250x250px renders of using the built-in math and the version using fancy math.

    Bhuddabrot image with included noise
    Bhuddabrot image with much of the noise removed

    The Impossible Task

    Here’s my line of thought: I needed to do arbitrary complexity calculations and get the really precise outline of a shape with an infinite curve.
    The problem: You cannot have both.

    The reason is simple. Calculating precisely is necessary to get a clear image. This blows up the memory usage. Suddenly the machine is n out of 5000+ iterations deep and out of RAM because it’s working with numbers of >=n*2 number of decimal places. It’s a technically solvable problem, but not in my lifetime.

    At this point, I wondered, is this how Physicists feel about trying to capture an electron? They can either get the position or velocity, but not both. 

    I either had to tell a lie to get an image or accept the limitations of floating-point number capabilities. That, or use all the compute power in the universe to calculate the shape of an infinite curve. Truthfully, I’m not opposed to telling a small lie. I consoled myself by putting a disclaimer on the finished product. 

    Since I had to lie, I wanted to make it as small as possible, within reason. I’m not afraid of a small computer bill. It’s entertainment dollars. Whereas telling the whole truth would easily consume my whole paycheck.

    The Final Code

    I had to figure out how to do arbitrarily precise calculations on complex numbers. Naturally, someone else had already done the hard work with the brick/math library. Unfortunately, it doesn’t support complex numbers. Since complex numbers are just a logical extension of real numbers, I was able to extend brick/math to support complex numbers in another library I wrote.

    The next step was removing all references to division. The division path rapidly consumes memory usage if not carefully contained. The answer is to use rationals. 

    “Oh boy, I’m smart”

    Me, Judah Wright, 2025


    The immediate next step was realizing that some numbers are going to be irrational by the nature of what we are trying to draw

    Suddenly, not feeling so smart.

    Some massaging between rationals where appropriate and decimals where irrational got me over the hurdle.

    “It’s only a small lie,” I told myself.

    Elevating To The Cloud

    I was not just throwing compute muscle behind a problem; I was being smart about it.

    The Work Queue

    Database Queue Driver

    Right out of the box, I needed a database to store the intermediate results. For that, I used the Laravel database queue driver.

    Unfortunately, the throughput of the job to queue the work is terrible. Terrible. Worse than local development on a sqlite database.

    $ time php artisan app:queue-bhuddabrot-jobs 1000 1000
    real    255m40.108s

    Memory Store Queue Driver

    A relational database is a bit overkill for the problem I was trying to solve. I was creating a big heap of work, then pulling the work off the queue as fast as we can. While this is doable with a relational database, I quickly hit a ceiling regarding how quickly the computer could pull items off the top of the pile.

    Here’s the catch though, adding and subtracting work from the queue was not the slowest part of the operation. Running the actual calculations was the slowest part. So, while there was room for improvement, optimization efforts were chipping away at a relatively small slice of the total time spent.

    The final render has 243 million jobs to process. If each job is 1.5kb that adds up to 364.5GB just to hold the tasks. To ensure I wouldn’t blow the budget in cache storage space, I wrote some code to throttle the queue size. This meant the process was tied to how fast the work could be completed.

    Infrastructure Benchmarks

    I launched two servers, a c8g.medium (1-core) and a c8g.large (2-core) – each was configured to run with 4 workers. That meant that there were 8 workers total spread across 3 compute cores.

    The “uppening” was upon us. If it worked, then all I’d have to do is create an auto scaling group of a proper size, worker number per core ratio, and maybe bump the database specs.

    A 1k render of the Bhuddabrot image

    It ended up taking 10 hours to generate a 1,000px by 1,000px image that seemed acceptable. I was a move in the right direction.

    Later, I ran a similar test, but tweaked the number of iterations for the red, green, and blue channels. I also set up an auto-scaling group to aim for 65% utilization on a maximum of 3 c8g.large spot-instance nodes, plus the c8g.medium “work dispensing” server. All 16 workers wrote to a t4g.small database back end. This test took around 7 hours from start to finish.

    Scaling this up wasn’t a problem. I just need to tweak the auto-scaling group maximum node count to go faster. I even bought spot instances to avoid paying full price.

    And then shit hit the fan.

    I queued up the final render, and set the auto-scaling group to allow up to 20 spot instances. A week and $75 later, the progress bar says it’s at 10%. The easiest 10%. An estimated $750+ compute bill for a pretty picture was not in the budget.

    According to the AWS billing dashboard, over the course of about 8 days I had sucked down 1368 computer hours. That’s 8 weeks of computer time. Surely, I’m using the wrong tool for the job.

    I called my buddy Chris for help. We talked about how this task might be a great fit for GPU computing, whereas PHP runs on CPUs. If I were to put this into production, where I’d be generating many images instead of just one big one, I think GPUs would be better suited for the job. It’s silly to draw fractals in PHP.

    Since PHP doesn’t run on GPUs, and doing arbitrary precision math on a different computing architecture sounds like a lot of learning, it seems this route is a dead end. This project was so close to being done! I didn’t want to start over from scratch.

    Back To The Drawing Board

    I stood back and beheld my creation. It had a work dispenser, a caching layer, work servers, and a database layer. What if, just like how lawn mower race drivers strip parts to go faster, I stripped parts off this monstrosity, just see how fast I could make it go.

    Websockets

    In the computer world, a “socket” is a special type of file. Scripts can both read from and write to a socket. They are unique in that they allow for inter-process communication, where one script can “talk to” another script. A “web socket” is the same process and idea, but instead of the special file being local, the special endpoint can be connected to over a network. 

    Websockets were the answer to how I was going to distribute the work. No database, no queue, just raw websockets.

    The server

    I slapped together some code to act as a “library” of sorts such that a worker could “check out” a unit of work. Then I put together a basic WebSocket server that would distribute work randomly from the library. Once a work unit has been “returned” with the trajectory for that point, the server updates the colors of the corresponding pixels in the resulting image.

    The client

    Finally, a client script connects to the server, checks out work, and returns the necessary calculations. The returned coordinates could be quite a lot of data, so some buffering logic was necessary to send long results incrementally. 

    Further Optimizations

    There were several optimizations along the way. The biggest optimization anyone can make is to simply “not do the work” in the first place. I thought more about the work being done. Each pixel input coordinate must have its calculation run three times with different iteration count limits. This is how you get the red, green, and blue channels.

    Instead of doing the same work 3 times with different iteration counts, I completed the work once and counted the outputs. Then the channels were colored red, green, and blue depending on the iteration count. This optimization alone cut the work to render each image to a third. Being able to calculate all color channels from a single trajectory calculation was a big milestone.

    Problems With Base 10

    Remember how I joked, “this rounding decision surely won’t haunt us later?” Well, now…

    The new system was blazingly fast but the image noise came back. I hunted for, found, and eliminated all sorts of bugs in the math libraries. I was running simulations at 500x500px. Occasionally I’d run a 1000x1000px calculation. But render after render gave the same noise.

    On happenstance, I wanted an intermediate quality image so I ran 768×768 and the noise was gone. There might be something there.

    I consulted ChatGPT. It helped guide me to the answer. 

    Turns out, that the overlaying grid is due to a small bias introduced with the combination of using a base-10 numbering system, and the necessity of rounding to the nearest base-10 decimal. This bias makes it subtly more likely that an escaping trajectory will land along a divisible-by-10 pixel space.

    The solution was so simple! 

    The render cannot be of a size divisible by 10. Illustrated below is a 500px and a 502px wide image. Note that not only do the lines disappear, but it is a much sharper image.

    500x500px Bhuddabrot with overlaying grid.
    502x502px Bhuddabrot with no overlaying grid visible.

    The Final Render

    Through the magic of “publishing when it’s done,” you didn’t have to wait for the week or so it took for the final render to render.

    A server monitoring dashboard observing several client workers.

    Anyways, enough stalling, here’s the final print, framed and ready to hang on the wall:

    Me sitting in front of a framed poster.
  • Sit With The Bees In Nature

    Hello internet traveler. Take a moment to relax. The bees will keep you company. The world will still be there in a few minutes.

    Sitting with the bees in nature for a few minutes.
  • Spring Bees Update

    We have had the bees for a few weeks now but I have not had the chance to post an update on how they are doing. Let’s use this blog post to catch everyone up.

    The NUC I ordered arrived on May 4th and was installed without incident. Upon subsequent inspections I didn’t find a queen or eggs, but the hive was raising some emergency queen cells so I figured I’d let nature take its course. The worst case scenario is they swarm, but they might do that anyways so no harm in letting them take care of themselves.

    The bee hive set up, there are bees guarding the entrance.

    We had a really nasty storm blow through a few days ago and the bees were mere feet from being squished. Literal feet from disaster, but the universe said “have a little treat” and they are safe.

    Two big limbs of a nearby tree crushing the fence of the apiary area. You can see leaning trees in the background.

    I gave them a look a few days after the storm to make sure they were OK. They were doing fine. They had packed their box full of nectar, and I mean full. Even the bridge comb they made to hold the lid on was packed full of sticky, syrupy nectar. Since they had filled out maybe 6-1/2 of their 8 frames I added an empty super.

    The next week (Yesterday relative to this blog post) I went to check on them. They seemed to be doing well, good numbers, saw young larvae and some brood. They had scraped the plastic foundation in the new super completely clean and used the wax in their bottom box. I’ll need to come back out with re-waxed frames to encourage them to draw the upper frames out.

    The last thing I noticed is their nectar stores had lessened over the week so I put up their feeder.

    The beehive with two supers and an entrance feeder installed.

    This morning I was curious how much was left in their feeder after one day and it was completely empty and dry. They wasted no time drinking it down, so I refilled it.

    I’ll have to melt down the bridge comb I pulled out of their hive and use it to re-coat some extra frames to hopefully encourage them to draw the foundation out. That could be its own blog post though!

  • We Survived A Tornado

    We Survived A Tornado

    Many of our trees were not so lucky.

    If you think I’m exaggerating, go look at a map of the tornado path. Far too close for comfort.

    Our trail camera caught a glimpse of the chaos after some limbs fell in front of the motion sensor.

    There are trees fallen every which way. That might go to show how close we were to being in even worse shape, if the wind was whipping around like that.

    The important thing is everyone is OK, including the bees. Nothing was lost that can’t be replaced.

  • Fishing For Bees

    The next step in the apiary journey is to have a home for the bee hives to live.

    The wooded area of the property seems a great fit, but the land is nearly impassable with the overgrowth in the summer months. To help keep a working area clear we fenced off an area, laid down a cardboard layer, used some reclaimed wood around the edges, then covered the whole thing in mulch.

    For the stand I spanned some wood between cinder blocks. I think it should do the trick!

    An image of the wooded area of our property. There are trees in the background. The foreground has a fenced area where the ground is covered in a brown mulch. There is a stand made of reclaimed wood and cinder blocks in the fenced area.

    Once done I set the swarm trap. I am using an “Interceptor Pro” I got off Amazon, and dabbled some “Swarm Commander” in it. Now we wait…

    Strapped to the stand in the fenced area sits the swarm trap.
  • Bee Box!

    You can’t have bees without having a home for them.

    After lots and lots of research I settled on an 8-frame Langstroth hive. I wanted to try a top-bar hive but I was given some good advice from a seasoned beekeeper that I should learn with the Langstroth style first. I also read some books (shout-out Thomas D Seeley, I don’t know him but have read his books) and learned that “Bees like to be cozy” so I opted for the smaller 8-frame instead of a 10-frame.

    I also grabbed a swarm trap, but that came already assembled so that’s not half as fun to show off.

    Now to catch some bees?

  • Granny’s Slippers

    Granny’s Slippers

    This pattern calls for two strands of yarn, pick out your favorite slipper colors. A multi-color and a mono-color yarn can pair well together.

    I use a US-9 / 5-1/2mm needle set and 4-ply yarn for this work.

    There are improvements that could be made, but this pattern holds true to her notes.

    [Sole]
    Cast on 29 stitches, leaving a long enough tail to stitch up the ankle.
    1. knit 9, purl 1, knit 9, purl 1, knit 9
    2. knit across
    .. repeat until 40 rows (women) or 48 rows (men)
    .. increase 1 row each shoe size, end on an even row

    [Toe]
    1. k2, k2 together, k1, k2 together, k2,
    purl 1,
    k2, k2 together, k1, k2 together, k2,
    purl 1,
    k2, k2 together, k1, k2 together, k2
    2. knit across
    3. k1, k2 together, k1, k2 together, k1,
    purl 1,
    k1, k2 together, k1, k2 together, k1,
    purl 1,
    k1, k2 together, k1, k2 together, k1,
    4. knit across
    5. k2 together, k1, k2 together,
    purl 1
    k2 together, k1, k2 together,
    purl 1
    k2 together, k1, k2 together,
    6. knit across

    Once done cut the thread leaving enough length to sew up the seam. Gather the threads on a needle and pull tight to close the toe, then sew up the seam.
    Ankle sizes differ, but stitch 1/3 to 1/2 of the way back to the base.

    Finally, stitch the ankle together using your initial thread and tie off.

    Repeat for the other slipper.

    A finished pair of slippers.
    A finished pair of slippers.
  • The Bee-venture

    I have decided to take up beekeeping.

    I told a few family members and now I have some supplies. There’s no turning back now!

    I mean look at me, I look ridiculous, I love it.

    Judah in his bee suit being investigated by his two dogs.

    I recently finished Honeybee Democracy and highly recommend it. I also got a copy of The Beekeepers Bible. I have been attending local (online because winter) beekeeping meetups trying to get my legs under me.

    My plan is to try and catch a swarm this spring. Wish me luck.

  • Honey Garlic… Again!

    A treat so nice I made it twice?

    So long story short I wanted to give another go at honey-garlic. I got some raw honey from the farmers market, grabbed some garlic from the grocery store, and found some adorable little jars for the fermentation process.

    The jars will take a week or two to bubble and do their thing. Eventually they will look something like this, a more amber color. At this point you should be using it before it gets old.

  • The Universe Entrusted Me With A Pumpkin

    Some time ago we had our lawn mowed. It’s a long story but we were in the process of saving up for a nice lawn mower and didn’t have the ability to mow our lawn ourselves. In any case, the lawn was mowed, then afterwards this mystery plant sprouted.

    Here’s my thinking. This plant saw an opportunity and took it. By the time we had a lawn mower it was too large to mow. So, since I can’t kill it, I must care for it. It’s not a pest if it’s a pet!

    Now we’re invested. The plant needs a name. Reginald sounds like a fine name. Here’s a few shots of Reginald as it grows.