Memory handling for fractal program

August 9th, 2009

I have been poking again at my fractal plotting program recently. The latest version is available here (source). I've added multi-threading (so all available cores will be used for calculation) and Direct3D rendering (which makes zooming much smoother and allows us to rotate in real time as well with the Z and X keys - something I've not seen any other fractal program do). There is still some jerkiness when zooming in or out (especially past a power-of-two boundary) but it's greatly reduced (especially when there's lots of detail) and with a little more work (breaking up the global matrix operations) it should be possible to eliminate it altogether.

The linked grid data structure in the last version I posted worked reasonably well, but it used too much memory. At double-precision, every block needed 72 bytes even after it had been completed. This means that an image that covered a 1600x1200 screen with detail at 16 texels per pixel for anti-aliasing would use more than 2Gb. I could use a different block type for completed points but there would still need to be at least 4 pointers (one for each direction) at each point, which could take 500Mb by themselves.

So I decided to switch to a data structure based on a quadtree, but with some extra optimizations - at each node, one can have not just 2x2 children but 2n by 2n for any positive integer n. When a node contains 4 similar subtrees (each having the same size and subnode type) it is consolidated into a 2n+1 by 2n+1 grid. If we need to change the type of one of the subnodes of this grid, we do the reverse operation (a split). In this way, if we have a large area of completed detail it will use only 4 bytes per point (storing the final number of iterations). This works much better - memory usage can still be quite high during processing (but seems to top out at ~700Mb rather than ~2Gb for the linked grid data structure) and goes right back down to ~200Mb when the image is completed (only about 35Mb of which is the data matrix, the rest is Direct3D textures).

One disadvantage of the grid tree method is that it doesn't naturally enforce the rule preventing 4 or more blocks being along one side of one other block. That rule is important for ensuring that we don't lose details that protrude through a block edge. However, this turns out to be pretty easy to explicitly add back in. This gives a more efficient solid-guessing method than I've seen in any other fractal program (by which I mean it calculates the fewest possible points in areas of solid colour).

This data structure was surprisingly difficult to get right. The reason for this is that with all the splitting and consolidating of grids, one must avoid keeping grid pointers around (since they can get invalidated when a split or consolidation happens in a nearby part of the tree). That means that many algorithms that would be natural to write recursively need to be written iteratively instead (since the recursive algorithm would keep grid pointers on the call stack).

Another tricky thing to get right was the memory allocation. My first attempt just used malloc() and free() to allocate and deallocate memory for the grids. However, the way grids are used is that we allocate a whole bunch of them and then deallocate most of them. However, they are quite small and the ones that don't get deallocated are spread throughout memory. This means that very little memory can be reclaimed by the operating system, since nearly every memory page has at least one remaining block (or part of one) on it.

The problem here is that the malloc()/free() interface assumes that a block, once allocated, never moves. However, grids turn out to be quite easy to move compared to general data structures. This is because the grid tree is only modified by one thread at once (it must be, since modifying it can invalidate any part of it) and also because we know where all the grid pointers are (we have to for split and consolidate operations). Only a dozen or so different grid sizes are currently used in practice, so it's quite practical to have a separate heap for each possible grid size. If all the objects in a heap are the same size, there's an extremely simple compaction algorithm - whenever we delete an item, just move the highest item in the heap down into the newly free space. So a heap consists of a singly-linked list of items. These are allocated in chunks of about 1Mb. Each chunk is contiguous region of memory, but chunks within a heap don't have to be contiguous. This allows the heaps to interleave and make the best use of scarce (on 32-bit machines) address space.

Sounds simple enough but again there turned out to be complications. Now it's not just split and consolidate that can invalidate grid pointers - now deleting a grid can also invalidate grid pointers. This means more recursive algorithms that need to be made iterative (such as deleting an entire subtree) and certain operations need to be done in a very specific order to avoid keeping more grid pointers around than are necessary. I now understand why such techniques are so rarely used - they're really difficult to get right. I do think that a general garbage-collected heap would be slower and use more memory than this special purpose allocator, but I'm not sure if difference would be large enough to make all this effort worth it. Perhaps someone will port my code to a garbage-collected language so the performance can be compared.

Sound for fractal program

August 8th, 2009

I'd like to be able to add some kind of sound output to my fractal program. What I'd like to do is generate a click every time a point is completed. So, when it's working on a difficult area it would emit a clicking sound but when it's finishing a lot of points it would generate white noise. The principle is very similar to that of a geiger counter's audio output.

This is trivial to do under DOS, but in Windows it is surprisingly difficult. Ideally we want to generate a waveform at 44.1KHz. When we finish a point, we flip the current sample in the audio buffer between its maximum and minimum positions. The problem is determining what the current sample should be (or, alternatively, moving a pointer to the next sample 44,100 times per second). What we need is a high-resolution timer, but none of the high-resolution timers seem to be particularly reliable. timeGetTime() is too low-resolution. QueryPerformanceCounter() has bugs on some systems which cause it to skip backwards and forwards a few seconds on occasion. The RDTSC CPU instruction isn't guaranteed to be synchronized between CPUs, and its count rate may vary due to power saving features on some CPUs. Another possibility is reading the playback cursor position from DirectSound, but that is unreliable on OSes before Vista.

What I may end up doing is faking it - measure the average number of blocks completed over some small period of time and use that to generate the clicks via a Poisson distribution.

Public medicine and medical research

August 7th, 2009

The US pays twice as much for healthcare per person as the average western country, and supplies a lower quality of care. So where does all that extra money go to?

Some of it goes to administrative overhead (it costs a lot of money to pay the people at your insurance company who are looking for excuses to deny you coverage). But a larger fraction goes towards the profits of the insurance companies. People invest in these companies (often as part of a 401k or other retirement plan) and get back a profit (at the expense of those who pay insurance premiums and consume healthcare).

But not all of a company's profits go immediately back to its investors - any reasonable company will spend some portion of their profits on research into ways to make even more money in the future. In terms of medical insurance companies, this generally means medical research - research into new techniques, medicines and gadgets that improve the state of the art and make us all healthier in the long run.

So if the US switches to public medicine, what happens to the state of medical research? If the massive profits go away, will the research money dry up too? I have heard concerns that socialized medicine will cause medical research to grind to a halt, leaving the entire world with 2009-level medical technology for the forseeable future. I don't think it will be quite that bad - there are other sources of medical research money than US insurance companies (the US government and other governments).

I hope that in the process of transforming the country to socialized medicine, the government continues the investment in medical research and changes its focus a bit. I have the impression that the direction of medical research is set at least somewhat by the treatments that will make the most money rather than the treatments that are medically best. Drug companies pursue research on drugs that they can patent, not new uses for existing drugs on which the patents have expired. Cures are pursued at the expense of prevention and finding causes. With a change in focus we can hopefully get more effective research for less money overall.

Why insurance is the wrong model for healthcare

August 6th, 2009

Insurance is a good model for things like car insurance and home insurance because the duration of a claim is very short - one never has to change one's insurance provider for reasons beyond one's control during a car crash or an earthquake. But a medical condition can last for many years or even a lifetime, during which there can be many individual claims.

Once you have a medical condition that is likely to take many years to resolve and be extremely expensive, you're beholden to your insurance provider, since you are a liability for them. Should you be allowed to change insurance providers? Okay, most of us can't change providers anyway without spending a lot more (to avoid paying tax on our health premiums we're stuck with our employer-provided health plans) but suppose that problem was fixed?

Let's suppose the answer is no, you shouldn't be allowed to change (at least not without taking a cripplingly large increase in premiums turning it from insurance into a payment plan) - no business should be required to take on a customer that is guaranteed to lose them money. Then the "free market" aspect of health insurance is denied to those people who need it most - it's not a free market when you can't make a choice.

So let's suppose the answer is that you should - that insurers should not able to deny access to insurance based on pre-existing conditions (which really ought to include genetic predispositions). Then insurers have to factor in the costs of such patients to all their customers' premiums. That doesn't sound so bad at first glance but consider the consequences - suppose that insurance company A had low premiums but covered very little and insurance company B had high premiums and covered everything. Then all the healthy people get insurance from A and switch to B when they get sick. B's prices then rise to cover the cost of their coverage and what you essentially end up with is equivalent to not having insurance at all. This means that government has to regulate what coverage is provided as well as who it is provided to. All insurance companies will then provide that level of coverage but none will provide any more. We've gone from having no choice to having no real choice. Either way we fail to have a free market for healthcare.

Socialized medicine aka single-payer healthcare aka politically controlled medicine has problems too. In the UK, different districts decide which treatments they will offer, resulting in a "postcode lottery" for some treatments. Waiting lists of years are not unheard of for treatments that are not imminently life threatening, and doctors and nurses are overworked and underpaid compared to their counterparts in the US. However, the average Brit pays about half what the average US resident pays in healthcare costs and gets better coverage on average. There is also no worry about medically-induced bankruptcy, and no worries about one's insurance company refusing coverage (due to a "pre-existing condition") once a serious medical condition is found.

I disagree that single-payer healthcare gives patients an incentive to over-consume. It should not be the insurance company that gets to decide that - doctors should be ones to decide which treatments are medically necessary. And if some doctors are over-prescribing compared to other doctors in their geographical area or speciality, that is something that should be investigated.

Another criticism of single-payer healthcare is that providers have no incentive to compete on price - the government will have to pay whatever they ask for critical treatments. But there's nothing special about single-payer healthcare there - there's nothing stopping the government from acting the same way as insurance companies to do to keep a check on prices. In fact the government has more power than an insurance company there - the insurance company only has the power to drop an overcharging provider from their preferred providers list while the government has the ability to take away their medical license.

Yet another criticism is that if the government gets to decide what treatments are on offer, people don't have any choice. Well, most of us don't have any choice anyway under the insurance model but at least with the government you can vote them out if they aren't doing a good job with healthcare. Yes, socialized medicine means a government bureaucrat between the patient and the doctor, but better an elected bureaucrat than an insurance-company bureaucrat that one has no control over.

378Kb disk hack for Fractint 15.1

August 5th, 2009

I first heard of Fractint soon after I got interested in Fractals. I knew it was very fast and had lots of fractal types and features and I wanted to run it on the PC1512 (the only PC I had regular access to at the time). I bought a "Computer shopper" magazine which had a copy of Fractint 15.1 on the cover disk. Only the 3.5" disk was available, so I had to get a friend who had both drives to copy it onto 5.25" disks for me. The disk contained a 323Kb self-extracting ZIP file which, when ran, produced (amongst other files) FRACTINT.EXE which was a 384Kb file. This would not have been a problem except that the machine only had 360Kb disks - FRACTINT.EXE wouldn't fit!

Somewhere (probably on another magazine disk) I found a program that was able to format disks to non-standard formats by calling BIOS functions (or possibly by programming the drive controller directly, I don't remember). It turns out that the drives in the PC1512 were actually able to format 42 tracks before the head bumped into the stop, not just the standard 40. This gave me a 378Kb disk, still not quite enough. I discovered that if I requested the program to format a 44-track disk, it would fail but record in the boot sector that the disk was 396Kb. With this format, I was able to persuade the ZIP program to try extracting FRACTINT.EXE. Of course, it failed to write the last few Kb but it turned out that because Fractint used overlays, it still worked - only one or two of the overlays were inaccessible (I think printing and shelling to DOS were broken, and I didn't miss them).

I made a Mandelbrot zoom video with this setup. It was on the point +i (zooming into a more detailed area would have taken too much time and disk space). I played the video back by converting the images to PCX files (which are much quicker to decode than GIFs) copying them to a ramdrive and then using PICEM to play them back at about 5fps.

Mandelbrot set taxonomy

August 4th, 2009

Lots of videos have been made of zooms into the Mandelbrot set. These videos almost all follow the same pattern. The initial image is the familiar one of the entire set. One zooms in on some tendril or other, gathering details, passing various minibrots and possibly some embedded Julia sets on the way until one reaches a final minibrot and the zoom stops.

The reason for this is that these sequences are relatively quick to calculate, they show a variety of the different types of behaviors that the Mandelbrot set has to offer, and they have a natural endpoint which causes the video not to seem like it has been cut off part-way through.

If we relax these conditions, what interesting zoom sequences open up to us?

To answer this question requires classification of the complex numbers according to the roles they play in the Mandelbrot set.

The first taxon separates exterior points (whose z sequences are unbounded, example: +1), interior points (limit of z sequence is a finite attracting cycle, example: 0) and boundary points (all other z sequence behaviours). Zooming into either of the first two will leave us with an image of uniform colour after a finite amount of time. Most zoom movies actually target the second of these.

Zooming in on the boundary will leave us with an infinite sequence of "interesting" images, so let's explore this group further. The second taxon separates shore points (those which are on the edge of an atom), Feignbaum points (which are at the limit of a sequence of atoms of increasing period) and filament points (all other boundary points).

Feignbaum points make quite interesting infinite zoom sequences. The canonical example is ~-1.401155, the westernmost point of the main molecule. Each time you zoom in by a factor of ~4.669201, the lakes look the same but the the filaments get denser. Filaments connect to molecules at these points. These are the only places filaments attach to the continent but each mu-molecule has one additional filament emerging from its cardioid cusp.

Filament points can be divided into two types based on orbit dynamics - those whose orbit is chaotic (irrational external angle) and those whose orbit converges to a finite repelling cycle. The latter includes two subtypes - branch points (where there are more than one but fewer than infinity paths to other points in the set) and terminal points (where there is only one). Well-known terminal points include -2 and +i. Zooming into terminal points and branch points gives a video which is infinite but which devolves into very same-y spirals after a short time as the mu-molecules shrink to less than a pixel.

I don't know an exact formula for any chaotic filament points, but they're easy to find using a fractal plotter - just zoom in, keeping away from any mu-molecules, branch points and terminal points. As with terminal and branch points, the minibrots tend to shrink to less than a pixel quite quickly, so the images obtained will become same-y after a while - the structures formed by the filaments tend to look like L-system fractals.

Shore points can be divided into two categories, parabolic points and Siegel points. Parabolic points come in two flavors, cardioid cusps (the point of maximum curvature of a cardioid, example: +0.25) and bond points (where two atoms meet, example: -0.75). If you zoom in a cusp you'll technically always have detail on the screen but the picture gets very boring very quickly as the cusps shrink to sub-pixel widths.

This leaves the Siegel points. You can find these by zooming into the edge of an atom but avoiding the cusps. For example:

  1. Start off with the large circle to the west (call it A) and the slightly smaller circle to the north (call it B).
  2. Zoom in until you can pick out the largest circle C between A and B on the boundary of the set.
  3. Rename B as A and C as B.
  4. Go to step 2.

This particular algorithm takes you to the point whose Julia set is known as the Siegel disk fractal, \displaystyle \frac{1}{4}(1-(e^{i(1-\sqrt{5})\pi}-1)^2) which is about -.390541+.586788i.

All points on the boundary of the continent cardioid can be found using the formula \displaystyle c=\frac{1}{4}(1-(e^{i\theta}-1)^2). For rational \displaystyle \frac{\theta\pi}{2}, this gives a parabolic point and for irrational \displaystyle \frac{\theta\pi}{2} this gives a Siegel point (a point whose Julia set has Siegel discs). The "most irrational" number (in the sense that it is least well approximated by rational numbers) is the Golden ratio \displaystyle \frac{1}{2}(1-\sqrt{5}). The Siegel point corresponding to this number is the point reached by the algorithm given above. In some sense then, this area is the most difficult area of the Mandelbrot set to plot, and indeed if you attempt to zoom into it you'll quickly discover that you have to increase the maximum number of iterations exponentially to avoid losing detail. I don't think it's really practical to explore this area very far without a Mandelbrot set program that can do SOI, such as Fractint, Fractal Witchcraft (which is a pain to get working now as it's a DOS program), almondbread (which is a pain to get working now as one of its prerequisites seems to no longer be available) or the program I'm writing.

If you could zoom a long way into it, what would the Siegel area of the Mandelbrot set look like? Well, we know the part inside the cardioid is inside the set. We also know it's not a bond point so at least part of the remainder is outside the set. The pattern of circles on the cardioid is self-similar (it's related to Farey addition) so the continent part would look similar to a shallow zoom on the same point. The rest is a bit hazy, though. My experiments suggest that the density and relative size of mu-molecules gets greater as one zooms in on this area, so an interesting question to ask is what the limit of this process is? Would the area outside the continent be covered with (possibly highly deformed) tesselating mu-molecules, leading to a black screen? Or would only part of the image be covered by mu-molecules, the rest being extremely dense filament structures? Both possibilities seem to be compatible with the local Hausdorff dimension being 2. Either way I suspect there are some seriously beautiful images hiding here if we can zoom in far enough.

Summary of the taxonomy:

  • Exterior points (example: infinity)
  • Member points
    • Interior points
      • Superstable points (examples: 0, -1)
      • Non-superstable points
    • Boundary points
      • Filaments
        • Repelling points
          • Terminal points (examples: -2, +/- i)
          • Branch points
        • Chaotic, irrational external angle filaments
      • Feignbaum points (example: ~-1.401155)
      • Shore points
        • Parabolic points
          • Cardioid cusps (examples, 0.25, -1.75)
          • Bond points (examples, -0.75, -1 +/- 0.25i, -1.25, 0.25 +/- 0.5i)
        • Siegel points (example: \displaystyle \frac{1}{4}(1-(e^{i(1-\sqrt{5})\pi}-1)^2) ~= -.39054087021840008+.58678790734696872i)

Bugs are good

August 3rd, 2009

This may strike horror into the hearts of the test-driven-development crowd, but it makes me nervous when I write a program and it works correctly the first time. Sure it works, but maybe the code is subtly broken in some way and it only works by accident. Maybe if you sneeze too loudly nearby it will blow up. Maybe if I add some feature it will blow up. Until I've given it a decent workout there's just no way of knowing.

But a program with a bug is a puzzle - solving the puzzle gives one purpose, and an opportunity to learn more about how the code works (or why it doesn't). Sometimes in the process of debugging I'll change parameters or add debugging code, exploring the design space in order to get a better idea about what's going on. Sometimes doing this will give me ideas for improvements unrelated to fixing a particular bug - a way to improve performance or a new feature to add.

I have much greater confidence in a program that I've fixed lots of bugs in than one that works first time.

Determining if a point is in the Mandelbrot set

August 2nd, 2009

I'm writing a super-fast, super-accurate Mandelbrot set plotter. This one is unusual in that no explicit iteration limit is ever set - it will keep iterating any point until it either escapes or is determined to be in the set. This works pretty well and produces much sharper cusps than any other program I've seen.

However, the downside is that it can take an arbitrarily long to determine if any given point is in the set or not. Points on the edge of the set are (as always) the particularly tricky ones - some of them exhibit chaotic behavior for a very long time before becoming periodic or escaping.

Currently I use two methods for determining if a point is in the set. I have explicit formulae for the main cardioid and the period-2 bulb, and I look for periodic orbits using Floyd's algorithm.

I'm wondering if a third method might be practical - one that determines if a point is in the set without waiting for it to become periodic. Here's the method I'm considering.

Let the Mandelbrot iteration function f(z) = z2 + c. Suppose we have a point c, and we suspect that it might be periodic with period N. We can determine if it is or not by finding a point zf that is unchanged under N applications of f, i.e. a solution to fN(zf) = zf. There will generally be 2N such solutions, of which only N will be relevant to the case at hand. However, if we have a z that is likely to be near one of these fixed points we should be able to converge on one of them quickly by using Newton's method or something similar.

Once we have our fixed point, we take the derivative of fN with respect to c and evaluate it at zf. This is easy to do numerically - just evaluate fN at zf and a nearby point, take the difference and divide by the original distance. If the modulus of the result is 1 or less, then the attractor is stable and c is in the set. If it isn't, the point could still be in the set as we might have picked the wrong zf or N - that is we are likely to get false negatives but no false positives using this method (which is exactly what we want - if we get a false negative we can
iterate some more, try a new N and/or a new zf and eventually get the right answer).

Working through these steps symbolically gives us closed form solutions for the period-1 and period-2 attractors but if we try it for period 3 we get a polynomial of order 6 in z (23 = 8 minus the two from factoring out the period-1 attractor) - I'm not sure if we can get a closed form solution for period-3 attractors from this.

The tricky part is choosing an appropriate N and an approximation to zf for any given point. I think the way to do it is this: whenever we find a periodic point with Floyd's algorithm, iterate a further N times to figure out the period, and store the period and fixed point. When we start a new set of iterations on a point, check to see if any neighbouring points are periodic. If they are, try this method using the N from that point and the zf (appropriately adjusted for the new c). When we find a periodic point this way, we store the found N and zf just as for Floyd's algorithm.

Installing Vista the difficult way

August 1st, 2009

I recently swapped the 100Gb hard drive that my laptop came with for a 320Gb model. I kept running out of space and deleting operating system components that I wasn't using, which is all very well except that when I tried to install Vista SP2 it complained that important files were missing. The SP2 installer doesn't really need those files either, it just does an integrity check.

After having installed the new drive I started to install Windows and then discovered that my laptop's DVD drive was in bad shape. It could read CDs fine but not DVDs at all.

I had read that it was possible to install Vista from a USB key so I tried this. Unfortunately the only USB key I have handy is a 1Gb one and Vista claims it needs a 4Gb one to install from. There doesn't appear to be anywhere close by that sells USB keys.

I wondered if it was possible to boot from the USB key, copy the required files to the hard drive over the network using the preinstallation environment and then install from the hard drive. With a bit of fiddling I got it to work, and here is how I did it.

  1. Format the USB key to NTFS, copy the files over but excluding install.wim. This only takes about 256Mb. Make the key bootable with "bootsect /nt60" and boot from it.
  2. Choose "repair" at the computer and open a command prompt. Use diskpart to partition and format the drive.
  3. Now we need to get the installation files onto the hard drive, including install.wim. We can get TCP/IP networking up and running with netsh but the Vista preinstallation environment doesn't include SMB file sharing. It does however include the ftp client, so we can set up a small FTP server another machine and then ftp install.wim over. The rest of the files can just be copied from the USB key.
  4. The next part is tricky - we need to convince the installer to run from the hard drive and also install to the hard drive. If we make the hard drive bootable the same way we made the USB key bootable and try to boot from it, the installer gets very confused and gets stuck in a loop after a couple of reboots. However, I hit upon another way which does work. Unmount the USB key and the hard drive with "mountvol /d" and then mount the hard drive with the same drive letter that was being used for the USB key. Then run "setup" and install Vista as normal.

Addendum: I tried to do this again with a new computer I built, but the Vista preinstallation environment didn't have drivers for my network card. Obviously if this is the case, another method must be used.

Complexity metric for identifiers

July 31st, 2009

As far as I know, every code complexity metric ever divised treats all identifiers equally. After all, they're all just opaque strings to the computer, right?

But complexity metrics are not designed for the benefit of computers - they're designed for people, to try to make the code less complex and easier for people to understand (otherwise we could just measure the length of the generated binary code).

And there is certainly something less complex about a variable called "cost" than a variable called "amountPaidPerItemInDollars" for example - a program using the second should surely be considered more complex than an otherwise identical program using the first, right?

On the other hand, one doesn't necessarily want to count all the letters in an identifier to measure its complexity - that would just lead to very cryptic or meaningless 1 and 2 letter variable names.

I think the answer is to divide identifiers up into words (in case sensitive languages by starting each word with a capital letter, and in non-case-sensitive languages by separating words with underscores). Each word counts for one point and should be a real word in the English language, or defined elsewhere in the program as a sequence of other words (perhaps with a special comment that the compexlity measurer can understand). So, for example, instead of having a lot of variables containing the characters hyperTextMarkupLanguage, one would have a glossary section in one's program saying "html == hyper text markup language" and then treat "html" itself as a word.

Making up terminology is an important part of programming, and one that I think is often overlooked. Giving a decent (appropriate, one word) name to each of the important concepts in your program from the get-go, and giving each of these terms a precise meaning (even if some details of those meanings change as the program evolves) causes one to be able to think about these concepts more clearly. It also leads to easier and more consistent naming of variables and types.