******************************* * * * FRMTUTOR.TXT * * * * AN INTRODUCTION TO * * THE FRACTINT FORMULA PARSER * * * ******************************* Written by Bradley Beacham CompuServe: 74223,2745 Revision 1.0 24 February 1995 ======= OUTLINE ======= 1.0 Legal Stuff 2.0 Acknowledgements 3.0 Limitations 4.0 Introduction 4.1 The Purpose Of This Document 4.2 My Assumptions 4.3 What Is The Formula Parser? 4.4 Formula Files 5.0 A Quote From Fractint.doc 6.0 Some Basics -- A Walk Through The Mandelbrot Set 6.1 Complex Numbers And The Complex Plane 6.2 The Mandelbrot Set 7.0 Anatomy Of A Formula 7.1 A Formula Is A Program 7.2 Elements Of A Formula 7.2.1 Formula Name 7.2.2 Symmetry Declaration 7.2.3 Braces 7.2.4 Variables 7.2.5 Functions 7.2.6 Calculation Expressions 7.2.7 Assignment Expressions 7.2.8 Comparison Expressions 7.2.9 Precedence And Parentheses 7.2.10 The Comma 7.2.11 The Semicolon And Comments 7.2.12 The Colon 7.3 Structure Of A Formula 7.3.1 The Name 7.3.2 Symmetry 7.3.3 Initializing 7.3.4 The Iterated Loop 7.3.5 The Bailout Test 8.0 A Walk Through A Pair Of Examples 9.0 Approaches To Writing Formulas 9.1 Using Mathematical Insights 9.2 Adapting An Existing Algorithm 9.3 Mutating An Existing Formula 9.4 The Monkey-At-The-Typewriter Approach 10.0 Style 11.0 Techniques 11.1 Speed-ups 11.1.1 Avoid Exponentiation And Function Calls 11.1.2 Avoid Unnecessary Calculations 11.1.3 Avoid Unnecessary Iterations 11.2 Simulating The If..Then Construct 11.2.1 How It Works 11.2.2 Pitfalls 11.3 Setting Defaults 11.4 Using Values From Previous Iterations 11.5 Dissecting A Formula With Algebra 11.6 Using A Counter 12.0 Problems 12.1 Potential Problems With Symmetry 12.2 Unparsable Expressions Ignored 12.3 Pathological Formulas 12.4 A Ghost Story 13.0 Where To Go From Here 13.1 Learn More About Complex Numbers 13.2 Learn More About Programming 13.3 Learn More About Fractals 13.4 Find Other Fractal Enthusiasts 14.0 Conclusion ================ 1.0 LEGAL STUFF ================ This document is Copyright (c) 1995 by Bradley Beacham. All rights reserved. I encourage you to copy and distribute it, so long as you leave it unchanged. It may NOT be used for commercial purposes without my explicit prior permission. I welcome any comments, questions, additional information and corrections. My addresses are: CompuServe: 74223,2745 Internet : 74223.2745@compuserve.com Post : Bradley Beacham 1343 S. Tyler St. Salt Lake City, Utah 84105-2122 U.S.A. ===================== 2.0 ACKNOWLEDGEMENTS ===================== Most importantly, thanks to Bert Tyler, the original creator of Fractint, and to Timothy Wegner and the rest of the Stone Soup Group for doing so much to improve it. Special thanks also to Mark Peterson for creating the original formula parser, and to Chuck Ebbert for making it go so much faster. A big thank you to all of the formula authors who have taught me so much through their examples, particularly Jonathan Osuch. A big thank-you to Ronald Black. Ron's excellent questions and thoughtful critiques have made this document much better than it would have been otherwise. Thanks also to Bob Carr, Jon Horner, Dan Parchman, David Walter and Lee Skinner (and others already mentioned) for making the effort to read preliminary versions of this document, and for offering insights, suggestions and encouragement. And finally, an unsolicited plug: If you don't already have a copy of FRACTAL CREATIONS by Timothy Wegner and Bert Tyler, get one. It covers much of the same material as this document, with the advantage of being written by the people most responsible for the development of Fractint. It also includes *lots* of stuff that isn't covered here. Several weeks after completing the first drafts of this document, I reread the book and realized that some parts of this tutorial are simply paraphrases of passages from FRACTAL CREATIONS; although I wasn't consciously imitating the book, the debt is obvious. In this case, imitation really *is* the sincerest form of flattery. ================ 3.0 LIMITATIONS ================ Except for quoted material, this document was not written by a Fractint programmer. I am a self-taught enthusiast, not a wizard. Consequently, you may find yourself disagreeing with my material, my conclusions, or my approach to the subject. If so, I look forward to hearing from you. It may be that we will end up disagreeing, but I am certainly willing to hear your critique. If you think that something needs to be added or corrected, please contact me and I'll attempt to fix the problems in a future version. Speaking of versions, bear in mind that I am using Fractint for DOS, version 19.0. Other versions of Fractint may render some of this material obsolete or inapplicable. Please don't be intimidated by the length of the document. You don't *need* to understand it all before you can get started and have a lot of fun. If you find it verbose, let me paraphrase Abraham Lincoln: "Sorry it's so long. If I had more time, it would have been much shorter." A word of warning for readers in the USA: I have adopted the European convention of placing trailing punctuation *outside* a quote. This seems especially appropriate when dealing with literal strings processed by the computer, but it also just makes more sense to me. I hope it doesn't bother you. ================= 4.0 INTRODUCTION ================= 4.1 THE PURPOSE OF THIS DOCUMENT --------------------------------- I wrote this in an attempt to fill a perceived gap. While much has been written about the Fractint program, the formula parser is still something of a mystery to some. Many people use the parser to create beautiful fractal images, but some (most?) have never even attempted to write a formula of their own. There are many possible reasons for this, but I believe one of the most pertinent is that the documentation on this subject in the standard Fractint package is rather terse. (This documentation is reproduced in section 5.0.) It provides important information, but it didn't exactly leave me with the feeling that I knew what to do next. Luckily for me, I plunged in anyway, and discovered that I could create my own formulas and have a wonderful time doing it. Most of my time spent with Fractint, by far, is devoted to fooling around with the formula parser. It's always exciting to create an interesting new image, but it's at least twice as satisfying to me when I find a beautiful image, full of wonderful complexity and chaos and order, coming from a formula that I wrote. And I do this despite the fact that I am *not* a highly trained mathematician. You really don't need to be a math wizard to write a Fractint formula! (Although if you are, so much the better.) All you really need is patience, persistence, and the willingness to learn. I learned a lot by reading FRACTAL CREATIONS. I also learned by reading files of formulas written by other people, trying to trace through the logic of the formulas and understand what they were doing. And some of what I learned came from simple experimentation and trial-and-error. Now I will try to summarize the most important things that I have learned so far. I hope this information will help potential formula authors get started. If I am successful, you will be spared much of the head-scratching that I endured along the way. 4.2 MY ASSUMPTIONS ------------------- I am assuming that you have a copy of the Fractint program, and that you have used it enough to know how to choose a formula from the menu system. I also assume that you want to try writing your own formulas. 4.3 WHAT IS THE FORMULA PARSER? -------------------------------- It is a part of the excellent fractal-generating program, Fractint. While Fractint has many different types of fractal formulas built into it, the formula parser allows you to add new fractals without having to change the program. These formulas are stored in simple text files, and may be viewed and edited by the user. 4.4 FORMULA FILES ------------------ The standard Fractint package supplies a file of formulas, FRACTINT.FRM, but other formula files may be used. For example, this document discusses many sample formulas which are found in the accompanying file, FRMTUTOR.FRM, and many other formula files (identified by the extension ".frm") may be downloaded from online services or BBSs. If you have an additional formula file, you should put it in the same directory that holds FRACTINT.FRM. To access it, choose the FORMULA type from the Fractint menu, and then hit the F6 key. You'll be shown a menu of available formula files; select the one you want and then you'll be able to use its formulas. This document refers to a few other formula files: FRACT001.FRM, BUILTN.FRM, FUBAR.FRM, OVERKILL.FRM and INANDOUT.FRM are available on CompuServe and other online services and BBSs. You won't need any of those files to follow the discussion, however. In addition to using formula files by other people, of course, there is another way to add new ones: WRITE YOUR OWN! The "how" is simple: All you need is a simple text-editor, such as the Edit program that comes with MS-DOS or the Notepad program that comes with Windows. Just be sure that your editor saves the file as simple unformatted ASCII text. Then follow the basic rules outlined below. The "why" is even simpler: Because it's fun. ============================== 5.0 A QUOTE FROM FRACTINT.DOC ============================== First, let's look at what Fractint.doc says about formulas: [BEGIN EXCERPT] (type=formula) This is a "roll-your-own" fractal interpreter - you don't even need a compiler! To run a "type=formula" fractal, you first need a text file containing formulas (there's a sample file - FRACTINT.FRM - included with this distribution). When you select the "formula" fractal type, Fractint scans the current formula file (default is FRACTINT.FRM) for formulas, then prompts you for the formula name you wish to run. After prompting for any parameters, the formula is parsed for syntax errors and then the fractal is generated. If you want to use a different formula file, press when you are prompted to select a formula name. There are two command-line options that work with type=formula ("formulafile=" and "formulaname="), useful when you are using this fractal type in batch mode. The following documentation is supplied by Mark Peterson, who wrote the formula interpreter: Formula fractals allow you to create your own fractal formulas. The general format is: Mandelbrot(XAXIS) { z = Pixel: z = sqr(z) + pixel, |z| <= 4 } | | | | | Name Symmetry Initial Iteration Bailout Condition Criteria Initial conditions are set, then the iterations performed while the bailout criteria remains true or until 'z' turns into a periodic loop. All variables are created automatically by their usage and treated as complex. If you declare 'v = 2' then the variable 'v' is treated as a complex with an imaginary value of zero. Predefined Variables (x, y) -------------------------------------------- z used for periodicity checking p1 parameters 1 and 2 p2 parameters 3 and 4 p3 parameters 5 and 6 pixel screen coordinates LastSqr Modulus from the last sqr() function rand Complex random number Precedence -------------------------------------------- 1 sin(), cos(), sinh(), cosh(), cosxx(), tan(), cotan(), tanh(), cotanh(), sqr(), log(), exp(), abs(), conj(), real(), imag(), flip(), fn1(), fn2(), fn3(), fn4(), srand(), asin(), asinh(), acos(), acosh(), atan(), atanh(), sqrt(), cabs() 2 - (negation), ^ (power) 3 * (multiplication), / (division) 4 + (addition), - (subtraction) 5 = (assignment) 6 < (less than), <= (less than or equal to) > (greater than), >= (greater than or equal to) == (equal to), != (not equal to) 7 && (logical AND), || (logical OR) Precedence may be overridden by use of parenthesis. Note the modulus squared operator |z| is also parenthetic and always sets the imaginary component to zero. This means 'c * |z - 4|' first subtracts 4 from z, calculates the modulus squared then multiplies times 'c'. Nested modulus squared operators require overriding parenthesis: c * |z + (|pixel|)| The functions fn1(...) to fn4(...) are variable functions - when used, the user is prompted at run time (on the screen) to specify one of sin, cos, sinh, cosh, exp, log, sqr, etc. for each required variable function. Most of the functions have their conventional meaning, here are a few notes on others that are not conventional. The function cosxx() duplicates a bug in the version 16 cos() function. Then abs(x+iy) = abs(x)+i*abs(y), flip(x+iy) = y+i*x, and |x+iy| = x*x+y*y. The formulas are performed using either integer or floating point mathematics depending on the floating point toggle. If you do not have an FPU then type MPC math is performed in lieu of traditional floating point. The 'rand' predefined variable is changed with each iteration to a new random number with the real and imaginary components containing a value between zero and 1. Use the srand() function to initialize the random numbers to a consistent random number sequence. If a formula does not contain the srand() function, then the formula compiler will use the system time to initialize the sequence. This could cause a different fractal to be generated each time the formula is used depending on how the formula is written. Remember that when using integer math there is a limited dynamic range, so what you think may be a fractal could really be just a limitation of the integer math range. God may work with integers, but His dynamic range is many orders of magnitude greater than our puny 32 bit mathematics! Always verify with the floating point toggle. The possible values for symmetry are: XAXIS, XAXIS_NOPARM YAXIS, YAXIS_NOPARM XYAXIS, XYAXIS_NOPARM ORIGIN, ORIGIN_NOPARM PI_SYM, PI_SYM_NOPARM XAXIS_NOREAL XAXIS_NOIMAG These will force the symmetry even if no symmetry is actually present, so try your formulas without symmetry before you use these. [END EXCERPT] ===================================================== 6.0 SOME BASICS -- A WALK THROUGH THE MANDELBROT SET ===================================================== 6.1 COMPLEX NUMBERS AND THE COMPLEX PLANE ------------------------------------------ First, a disclaimer: This document is not intended to be a complete course on complex math. If you want to learn more about complex numbers, find a good algebra text. FRACTAL CREATIONS also has a good summary of the math involved here. But even if you are unfamiliar with complex numbers, read on. Don't be intimidated by the word "complex"! Let's go over some of the fundamental concepts that you'll need to get started. Since you already have a copy of Fractint, you have undoubtedly spent some time exploring the Mandelbrot set (M-set), easily the most famous fractal. By reviewing some of the details about how this fractal is generated, you'll be better equipped to imagine new varieties of fractal formulas. This fractal is called a "set" because it is a set of points on a two-dimensional plane, somewhat like the graphs you probably had to draw in your algebra classes. The Mandelbrot set exists in the "complex plane", so-called because it is composed of complex numbers. You should know that a complex number has two parts: the real and the imaginary. Just as the real number system is the union of the rational and the irrational number sets, so the complex number system is a union of the real and the imaginary numbers. An imaginary number is any real number multiplied by the square root of -1. This square root of -1 has a special name: i. So a complex number which had a real component 8.5 and an imaginary component 3.2 could be written as 8.5 + 3.2i, or in parser notation as (8.5,3.2). And because the reals are a subset of the complex numbers, any real number is also a complex number; that is, 2 = 2 + 0i or (2,0). You can perform arithmetic with complex numbers; addition, subtraction, multiplication and division are all possible, and follow the rules of basic algebra with 'i' being treated as a variable. Fractint also supports exponents (X^Y means X to the power of Y) and a variety of functions that operate on complex numbers, such as sin(), tan(), etc. I won't belabor this subject further for now, except to point out that when a complex number is operated on mathematically, both the real and the imaginary parts of the number may change; this concept is important in the discussion that follows. (A further discussion of complex arithmetic can be found in section 11.5, "Dissecting A Formula With Algebra", and functions are described in section 7.2.5, "Functions".) In the complex plane, the horizontal axis corresponds to the real number line while the vertical axis corresponds to the imaginary number line. Any particular complex number, therefore, can be plotted as a point on the plane, and any point on the plane has a complex number that corresponds to it. The real part of the number determines the horizontal placement of the point, and the imaginary number determines the vertical. The origin of the graph (the place where the axes cross) is 0 + 0i. 6.2 THE MANDELBROT SET ----------------------- As a prelude to examining formulas, let's look at the processes involved in deciding whether a particular point on the complex plane belongs to the M-set. Since the concepts involved are somewhat abstract, we'll try to create an analogy that is easier to visualize, and talk in very general terms at first. Imagine a circle drawn on the ground, with a little ball sitting in the center. We'll pick a spot on the ground, somewhere within the circle, and call that the "test point". Now we will start moving the ball in discrete steps according to a set of specific rules (which we won't describe yet) and watch the path that the ball takes. The first step always moves the ball over to our test point. The second step moves it to a different location, and the third step to yet another location. We'll keep applying the rules of movement, over and over, calculating a new position for the ball each time, and counting the number of moves we make. If we try this process for several different test points, we will see something very interesting. For some test points, the ball seems to settle into a fairly predictable path, something like the orbit of an object in space -- it moves from spot to spot, but it never strays outside of the circle drawn on the ground. For other test points, the ball may move around within the circle for a while and then exit. And for some test points, the ball leaves the circle after very few moves. Now let's try to categorize the different test points, according to the behavior of the moving ball. If the ball never leaves the circle, we'll color the test point blue, but if the ball *does* leave the circle then we'll give the test point a different color, based on the number of steps required make the ball cross the boundary. If you did this for enough test points, an image of the Mandelbrot set would appear! Let's move beyond our analogy now and get more specific. Instead of the ground, visualize the complex plane, and instead of a ball, visualize a moving point called 'Z'. Now picture a circle on the plane, centered on the origin, with a radius of 2. First, we'll choose a point to test; let's say 0.2 + 0.5i. Next we must define two complex variables, Z and C, such that Z = 0 + 0i and C = the value of the test point, ie C = 0.2 + 0.5i. Then the following algorithm is iterated (repeated over and over): Calculate the value of Z^2 + C, and then place the result in Z. Since Z has a new value, find the point on the complex plane that corresponds to Z, and then check to see if the distance between Z and the origin exceeds 2. If the distance is greater than 2 (Z is outside of the circle) then the test point is *not* in the Mandelbrot set, and you may stop calculating values for Z. But if Z remains in the circle, we move back to the top of the loop and calculate a new value for Z and check it again. In our example, if we start with Z = 0 + 0i and C = 0.2 + 0.5i, after the first time through the loop we now see that Z = 0.2 + 0.5i. Since this falls within the "bailout" circle we will calculate again, with the result that now Z = -0.01 + 0.7i. The next iteration ends with Z holding the value -0.2899 + 0.486i. We could repeat this process over and over, noting that Z shifts its position with each iteration and yet never exits the bailout circle. But because our time and patience have limits, we couldn't (and wouldn't want to) repeat the experiment an infinite number of times! This is where the value for maximum iterations, set on Fractint's menu, comes in. The default value for maximum iterations is 150. This means that if the program goes through the iterated loop 150 times, and Z has never strayed outside the bailout circle, Fractint *assumes* that the test point (C) is indeed part of the set, colors it accordingly, and moves on to another test point. Now what if the test point had the value 1.5 - 1.2i? After the first iteration, Z = 1.5 - 1.2i, which is still barely within the bailout circle. After the second iteration, Z = 2.31 - 4.8i. This time Z has strayed out of the circle, so the test point is *not* part of the M-set. Because the bailout condition has been met, Fractint stops iterating the formula and colors the test point. By default, Fractint chooses a color based on the number of iterations required to make Z exit the bailout circle, but this can be changed by various options on the Fractint menus. I have mentioned two conditions that cause Fractint to stop looping through the formula: 1) the bailout condition is met, and 2) the maximum number of iterations has been reached. There is one other condition that can cause Fractint to stop iterating: periodicity. If Fractint detects that Z has fallen into a periodic loop, repeating the same values over and over without leaving the bailout circle, it reasonably assumes that Z will *never* exit the circle and stops iterating even though the maximum number of iterations may not have been performed yet. This is one of the reasons that Fractint is so much faster than other fractal programs you may have tried. We're almost there, but before Fractint can create a picture of the Mandelbrot set it must settle a couple of problems, both of which have to do with infinity. First, you should see that in the complex plane there is an infinite number of points. Obviously Fractint can't test them all. So, it chooses a subset of the points, defined by the corners of your zoom box, and only considers points within that box. But even within that box, there is an infinity of possible points to test. Here, the resolution of your computer display is used to resolve the problem. Remember that a picture on your screen is composed of little dots called pixels. Fractint chooses points on the plane that correspond to the locations of the pixels, and only tests those points. (It can create images at a higher resolution than your display via the "disk-video" modes, but we won't go into that.) One point per pixel is enough. So now we have a finite number of points to test. Fractint moves from pixel to pixel, finding the value on the complex plane that corresponds to each pixel and performing the test loop. Pixels are colored dark blue (by default) if they are deemed part of the M-set, and a different color (normally based on number of iterations needed to exit the bailout circle) if they are not. We can instruct Fractint to do all of this (and more) with the following formula: Mandelbrot (xaxis) { ;The classic Mandelbrot set z = 0, c = pixel: z = z*z + c |z| < 4 } ========================= 7.0 ANATOMY OF A FORMULA ========================= 7.1 A FORMULA IS A PROGRAM --------------------------- Perhaps the most fundamental point I could make is that a Fractint formula is actually a little computer program, not a set of mathematical equations. If you don't take this into account, you will end up very confused! For example, consider the following statement: z = z + 1 Interpreted as an equation, that's nonsense. Instead, it is a program statement that means "Calculate the value of z + 1, and then set z to equal that value." Variables in a Fractint formula can, and often do, change values from one iteration to the next. 7.2 ELEMENTS OF A FORMULA -------------------------- Let's look at some formulas and see what parts they may have. We'll start with the following formulas: frm-A (xaxis) { ;Another formula for the Mandelbrot set z = const = pixel: z = z^2 + const |z| < 4 } frm-B { ;A generalized Julia formula ;For the traditional Julia algorithm, set FN1() to SQR, ;and then try different values for P1 z = pixel: z = fn1(z) + p1 |z| <= (4 + p2) } [7.2.1 FORMULA NAME] Each formula begins with a name, so that you can select it from the Fractint formula menu. [7.2.2 SYMMETRY DECLARATION] You should notice that frm-A contains a declaration of symmetry: (xaxis). This is an instruction to Fractint that says, in effect, "The images created by this formula will be symmetrical around the X axis, so use the appropriate symmetry-drawing technique". After being told this, Fractint will (when possible) use the following shortcut: It will only iterate the formula for pixels that fall on one side of the X axis. After testing a pixel and coloring it, it will "mirror" the result by finding the corresponding pixel on the opposite side of the X axis and giving it the same color. So at the default zoom, only *half* of the pixels need to be tested! Judicious use of this technique speeds up the program, but it can cause problems if not used correctly. I'll talk about this in more detail later. [7.2.3 BRACES] The braces define where the body of the formula begins and ends. There should be just one opening brace ({) and one closing brace (}) per formula. [7.2.4 VARIABLES] Examples in these formulas include z, c, and pixel. Variables in Fractint formulas are always of the complex number type. You may give them any names you like, but there are a few predefined variables: pixel, p1, p2, p3, lastsqr, rand, and z. Pixel, of course, gets the complex value corresponding to the current pixel. P1, p2 and p3 (if used) can have their values set by the user at the menu. Lastsqr was used in a speedup technique in the earlier days of the parser, but is seldom used now. Rand can be used if a random complex number is desired. It doesn't matter whether you use upper or lower case; "pixel", "PIXEL" and "Pixel" all refer to the same variable. Z is the name you should give to the primary variable; usually this is the variable that is tested at the end of each iteration. Naming it 'Z' is not *required* but it is highly recommended, because Fractint's periodicity testing is set up to look for patterns in the values of Z. You may wonder about the difference between Z and |Z|. Those '|' characters change the meaning completely. While Z is just the name of a variable, |Z| tells Fractint to determine the distance (the modulus) between Z and the origin; this is why it is used in the bailout test. In actuality, Fractint calculates the *square* of the distance, so the '|' characters are sometimes called the "modulus squared" operator; more on that in a moment. Let's use a concrete example. Suppose Z has a value of (3,-4), or 3 - 4i. We can use the Pythagorean theorem to determine the distance between this point and (0,0). If you were to find this point by first drawing a horizontal line from (0,0) to (3,0), and then a vertical line from (3,0) to (3,-4), you would have two legs of a right triangle, while the line from (3,-4) to (0,0) would be the hypotenuse. Draw this out on a piece of graph paper if it isn't clear so far. Now the Pythagorean theorem states that the sum of the squares of the legs will equal the square of the hypotenuse. This means the distance from the origin to Z can be calculated as the square root of (3^2 + -4^2), which works out to 5. Obviously, this Z has strayed outside the bailout circle! Fractint uses a small modification of the system I just described. Instead of checking to see if square_root(x^2 + y^2) < 2, Fractint checks if x^2 + y^2 < 2^2. These two expressions are mathematically equivalent. The advantage of the second way is that Fractint can avoid calculating the square root, which is much harder to do than calculating a square! This is another way that Fractint speeds up the calculation process. It also explains why the bailout tests in the Mandelbrot and frm-A formulas are written "|z| < 4" rather than "|z| < 2". This "modulus squared" technique is a bit subtle, and can lead to some confusion if not properly understood, but it has speed benefits that few of us would want to give up! Although P1, P2 and P3 have predefined names, the values of these variables can be chosen by the user when the formula is first selected or via the menu. For example, in the frm-B formula, P1 is used as a user-determined constant that is added to Z each iteration, while P2 varies the bailout condition: the radius of the bailout circle will be the square root of (4 + p2). You can add other variables with names of your choice. If you'll compare frm-A with the Mandelbrot formula, you'll see that (among other differences) there is a variable called 'c' in Mandelbrot and a corresponding variable called 'const' in frm-A. These variables serve exactly the same purpose -- they just have different names. The variable name 'c' is traditionally used in Mandelbrot formulas, but Fractint does not require it. All of this is just to illustrate that you have the power to choose your own names for your variables. It is good practice to avoid confusion where possible; one way to help is by using descriptive variable names. But as I noted above, use the variable name 'Z' for your "main" variable whenever possible. [7.2.5 FUNCTIONS] Fractint has several functions built into it, including sin(), cos(), and so on. A list of them appears at the end of this section. A function is something like a little machine -- you give it a number, it performs some operations on it, and then it gives you back a (usually) different number. Each function has a name, and is immediately followed by a pair of parentheses. Within these parentheses you should put the variable or expression that should be "fed" to the function. For example, "SQR(Z)" means "calculate the square of Z". Functions can be nested, and the results of the inner function will become the input for the outer function. For example, "COS(SQR(Z))" means "calculate the square of Z, and then find the cosine of the result". You can explicitly write specific functions into your formulas. For example, a formula for the Mandelbrot set might include the expression "z = sqr(z) + c". Another option is to include user-selectable functions, as in frm-B. You may include up to four different ones, and they are designated FN1() ... FN4(). They are given specific values on the same screen as P1, P2 and P3; that is, the menu. So the example from the previous paragraph could also be written "z = fn1(z) + c". Of course, this would require the user to set FN1 to SQR in order to make the M-set; other functions would give different results. With Fractint version 19.0, there are 26 functions available via the user-selectable functions! The user-selectable functions are a double-edged sword. On the one hand, they allow for much more flexibility while exploring, because a formula becomes capable of creating many different kinds of fractals. Using combinations of user-selectable functions multiplies the possibilities; a formula that uses just two of them is the equivalent of 676 different formulas with hard-coded (explicitly written) functions, while a formula that uses all four is the equivalent of 456,976 hard-coded formulas! In this way, a formula really becomes a "formula template" and helps you save space and time. On the other hand, this sort of formula can be confusing to use, especially for the new or casual user. Someone who just wants to explore the Mandelbrot set may not appreciate being asked to know and remember that they must set FN1 to SQR in order to get what they are looking for. There may be incredible images buried within such a formula, but they require the user to do some digging to get to them! I must confess a tendency to go hog-wild with these user-selectable functions, especially in my earlier efforts. My more recent formulas typically just use two of them. In most cases two should be plenty, I think, but of course that's entirely up to you. Now here's a list of the user-selectable functions. Remember that we are using the complex number system here, so many functions are different from (but related to) the standard trigonometric functions that you might know about; more information about these functions can be found in the Fractint documentation. Also note that some of the comments use the word "argument" -- this is the number that is "fed" to the function. abs() ---- Real and Imaginary Absolute Value. Returns the argument after making sure both the real and imaginary parts are positive. Abs(-3,-4) == (3,4). acos() --- Arccosine. acosh() -- Hyperbolic Arccosine. asin() --- Arcsine. asinh() -- Hyperbolic Arcsine. atan() --- Arctangent. atanh() -- Hyperbolic Arctangent. cabs() --- Complex Absolute Value. Returns the distance between the complex number and the origin. Cabs(-3,4) == 5. conj() --- Complex Conjugate. Returns the argument after reversing the numeric sign of the imaginary part. Conj(1,-3) == (1,3) and conj(1,3) == (1,-3). cos() ---- Cosine. cosh() --- Hyperbolic Cosine. cosxx() -- When the cos() function was first added to Fractint, it had a programming bug. After the bug was discovered, the corrected cos() was added, but the original function was retained under the name cosxx(), so that formulas and images made with the original function could be recreated. Cosxx() returns the same value as cos(), except that the sign of the imaginary part is reversed. Cosxx(z) == conj(cos(z)). cotan() -- Cotangent. cotanh() - Hyperbolic Cotangent. exp() ---- Exponential. flip() --- Returns the argument after swapping the values of the real and imaginary parts. Flip(1,-3) == (-3,1). ident() -- Identity. Returns the argument unchanged. Suppose a formula contains the expression "fn1(z*z) + c", but you just want to see the results of z*z + c. You can do this without rewriting the formula by setting fn1() to ident(). Ident(z) == z. log() ---- Natural Log. recip() -- Reciprocal. sin() ---- Sine. sinh() --- Hyperbolic Sine. sqr() ---- Square. sqrt() --- Square Root. tan() ---- Tangent. tanh() --- Hyperbolic Tangent. zero() --- Returns 0. This allows you to "turn off" an expression without rewriting the formula. If you were using a Mandelbrot mutation with the iterated section "z = z*z + c + fn1(z)", you could see the normal Mandelbrot set by setting fn1() to zero(). Zero(1,-3) == (0,0). A few other functions can be hard-coded, but aren't available through the user-selectable functions. imag() --- Returns the imaginary part of the argument as a real number. The imaginary part of the returned value is zero. Imag(1,3) == (3,0). real() --- Returns the real part of the argument. The imaginary part of the returned value is zero. Real(1,3) == (1,0). srand() -- Uses the argument to "seed" the random-number generator. [7.2.6 CALCULATION EXPRESSIONS] It's hard to imagine a formula where no calculations take place. These calculations are defined in expressions such as "z*z + c". You can find a list of the allowed mathematical operators in the Fractint documentation. [7.2.7 ASSIGNMENT EXPRESSIONS] After you calculate something, you'll often want to store the answer somewhere. To do this you should use the assignment operator: '='. For example, the expression "A = B + C" means "Find the value of B + C, and set A equal to that value." You can also "chain" assignments, as I did in frm-A. The expression "z = const = pixel" results in both 'z' and 'const' getting the value of the variable 'pixel'. [7.2.8 COMPARISON EXPRESSIONS] Often you'll want to compare one value to another, to determine what should happen next. The most common example is the bailout test. Comparison expressions take such forms as: A < B (is A less than B?), A >= B (is A greater than or equal to B?), or A == B (is A equal to B?). There are a couple of things you should know about the way Fractint makes comparisons between complex numbers. First, be aware that only the *real* parts of complex numbers are compared. To Fractint, if A = (1,1000) and B = (2,1), then A < B. Please note that we are comparing the *values* of complex numbers here, not the *distance* of those numbers to the origin. So for this case A < B, but |A| > |B|. Second, you should know that comparisons will always evaluate to either TRUE or FALSE. This may seem like a simple-minded observation, but we'll come back to it later. [7.2.9 PRECEDENCE AND PARENTHESES] Precedence means that Fractint has a preferred order for performing mathematical operations. Multiplication has a higher precedence than addition, for instance. This means that the expression "5 + 2 * 3" would evaluate to 30, not 21, because the multiplication will take place before the addition. But suppose 21 is the answer that you really intended; now what? The answer is to use parentheses to override the "natural" precedence order, as in "(5 + 2) * 3". You'll find a table of the precedence order in the quote from the Fractint documentation. Parentheses can also be used simply to make the meaning of a complicated expression clearer to you and your readers. Just remember that your parentheses *must* come in matched sets. For every '(' there must be one ')'. [7.2.10 THE COMMA] The comma (,) lets you put more than one expression on a single line without confusing the parser. These two formulas are logically and functionally the same: frm-C1 { z = 0 c = pixel: z = sqr(z) + c |z| < 4 } frm-C2 { z = 0, c = pixel: z = sqr(z) + c, |z| < 4 } Although functionally the same, you may prefer the "style" of one over the other. We'll return to style in section 10.0. In some formula files, you may notice formulas in which almost every line ends with a comma or semicolon. (For examples, look at Cardioid and CGNewtonSinExp.) I have been told that an earlier version of the parser required commas or semicolons to separate the lines, but this requirement was subsequently removed. The practice continues, though, apparently spread by simple imitation. (I say this with some confidence because I did the same thing at first.) While ending a line with a comma or semicolon is not necessary, it shouldn't cause any harm either. My preference is to use them only when needed. [7.2.11 THE SEMICOLON AND COMMENTS] The semicolon (;) tells the parser, in essence, "From this point to the end of the line, ignore everything. It is not to be calculated." This allows you to add comments to your formulas. Do it! It makes your formulas easier for someone else to understand, and it may help you understand your own formulas later on, after they are not so fresh in your mind. [7.2.12 THE COLON] The colon (:) has only one function in a Fractint formula (so far as I know) and that is to mark the end of the initialization section and the beginning of the iterated loop. I'll explain that in more detail in the section that follows. 7.3 STRUCTURE OF A FORMULA --------------------------- Now that we've looked at some of the parts we can use, let's talk about how to put them together into a working formula. In my opinion, any formula can be divided into at least three sections and at most five, with four being the most common arrangement. The three required sections are 1) the name, 2) the initialization section, and 3) the body of the iterated loop. Also present in almost all formulas is 4) a bailout test. The section most often omitted is 5) the symmetry declaration. Our Mandelbrot formula has all five: Name Symmetry | | V V Mandelbrot (xaxis) { z = 0, c = pixel: <-- Initialization z = z*z + c <-- Body of loop |z| < 4 <-- Bailout test } [7.3.1 THE NAME] Any formula must have a name, or else Fractint will not be able to find it. You have a lot of latitude in choosing a name for your formula, but there are a few limitations. The characters in a formula name must be contiguous; if you tried to name a formula "my new formula", it would appear on the formula menu as "my". (Working alternatives include "my_new_formula" or "MyNewFormula".) Also, be sure that you don't put two formulas with the same name into a formula file; they'll both appear on the menu but only one of them will be available. Finally, avoid using formula names longer than eighteen characters. At best, Fractint ignores the extra characters, but in my experiments I have locked-up Fractint with too-long formula names. [7.3.2 SYMMETRY] The parser reads this part of the formula, if present, just *once per image*. In it, you tell Fractint to assume that a formula will produce fractals with a certain kind of symmetry and Fractint will simply take your word for it, with the resulting images being drawn more quickly. There are dangers involved, though. I'll talk about them in section 12.1, "Potential Problems With Symmetry". [7.3.3 INITIALIZING] The initialization process takes place just *once per pixel*. This is the part of the formula where the parser sets up variables and gives them initial values. Any uninitialized variables start out with the value (0,0). The initialization section begins after the opening brace and extends to the colon. (Be careful to only use one colon per formula. Depending on its location, an extra colon may trigger error messages or cause the formula to behave in unintended ways. At best, the redundant colon is ignored.) In our Mandelbrot example, the following happens once per pixel: Z is set to 0 (since the imaginary part is unspecified, it is also set to 0) and C is set to the value of PIXEL. Recall that PIXEL gets its value automatically from Fractint. [7.3.4 THE ITERATED LOOP] This is where the real computational action takes place. The iterated loop begins immediately after the colon and extends to the end of the formula. The entire section is repeated over and over until 1) the bailout condition is met, 2) the maximum number of iterations have taken place, or 3) periodicity has been detected. If you are familiar with the DO/WHILE loop construct found in Pascal, C, and other languages, then you will understand how a parser loop works. The entire body of the loop is performed at least once, and then the parser decides whether it is appropriate to loop again (that is, move back to the colon) or to quit iterating. [7.3.5 THE BAILOUT TEST] Although this is technically a part of the iterated loop (because it is performed once per iteration) the bailout test warrants further description. Look at the bailout test for the Mandelbrot formula: |z| < 4. From our previous discussion of how the Mandelbrot algorithm works, we can see that Fractint interprets this to mean "If the modulus squared of z is less than 4, then perform the loop again." In other words, if the answer to the bailout test is *false*, then it is time to bail out of the loop. I should point out that it is possible to write a formula that has no bailout test, but I don't recommend it. I'll come back to this subject in section 12.3, "Pathological Formulas". ====================================== 8.0 A WALK THROUGH A PAIR OF EXAMPLES ====================================== Now let's try to tie all of the parts together by looking at some examples in detail. The next two formulas are taken from FRACT001.FRM, with some comments added. Cardioid { ;author not listed z = 0, x = real(pixel), y=imag(pixel), c=x*(cos(y)+x*sin(y)): z=sqr(z)+c, |z| < 4 } CGNewtonSinExp (XAXIS) { ;by Chris Green ; Use floating point, and set P1 to some positive value. z=pixel: z1=exp(z), z2=sin(z)+z1-z, z=z-p1*z2/(cos(z)+z1), .0001 < |z2| } These two formulas have some similarities, but their differences are especially interesting. Let's look at some of the differences, section by section. First, notice that CGNewtonSinExp declares XAXIS symmetry, while Cardioid has no symmetry declaration. This part is always optional. Next, compare the initialization sections. In CGNewtonSinExp, this section is very simple, but in Cardioid it is much more complicated. Let's trace through Cardioid's "per-pixel" section. First, z is set to 0. Next, the "real" function is used. This function takes a complex number as its argument and returns the value of the real part of the number. So the real part of x is set to equal the real component of pixel. Similarly, the real part of y is set to equal the imaginary part of pixel. Finally, c gets a value based on a rather complicated looking expression that involves x and y and a pair of functions. Now think back to the discussion of the Mandelbrot set. Remember that to complete an image, Fractint performs a set of computations (including the iterated loop) for each pixel of the image, moving from one pixel to another as the test point. This means that each time the parser executes the initialization section of Cardioid, the variable "pixel" will have a different value. This in turn means that the value of c will vary from one pixel to another. By contrast, the initialization section of CGNewtonSinExp is utter simplicity: z gets the value of pixel. Now look at the iterated sections of each formula, and remember that this section extends from the colon clear down to the end of the formula. Here the tables are turned. Cardioid has a very simple iterated section - just one line plus the bailout test. CGNewtonSinExp requires three lines to complete a comparatively complicated set of calculations before performing the bailout test. Four variables and three functions are involved, plus addition, subtraction, multiplication and division. Finally, let's look at the bailout tests. Cardioid has the familiar test, "|z| < 4", which means "Stop iterating if the distance between z and the origin exceeds 2". (Re-read the passage in section 7.2.4 describing the modulus-squared operator if this isn't clear.) Broadly speaking, this sort of test says "Count how many times the formula must be iterated before z heads off in the direction of infinity." And for points that are not part of the set, z tends to do just that: the different color-bands of the standard M-set image reflect how many iterations were required before z crossed the line. Fractals based on this general algorithm are sometimes called "Escape-Time To Infinity" fractals, because membership in the set is based on whether or not z "escapes" from the bailout circle. By contrast, look at the bailout test for CGNewtonSinExp, ".0001 < |z2|". Although the tests may look similar at first glance, there is something fundamentally different here. In this formula, the parser is instructed to keep iterating as long as |z2| *exceeds* the value .0001; that is, as long as it is *outside* the bailout circle. This kind of formula is sometimes called "Escape-Time To A Finite Attractor", and is used in the various "Newton" and "Halley" fractal types found in Fractint. Let's recap by tracing through both formulas once more, looking for details that we may have missed the first time through. First, when Cardioid begins, Fractint notes the location of the corners of the zoom box, so the complex values that correspond to the pixels may be found. (This is not specified by the formula. Fractint does it automatically when the parser is used.) Then for each pixel, the initialization section is performed just once; remember that this section extends from the opening brace down to the colon. While this section of Cardioid is comparatively complicated, the fact that it is performed just once per pixel means it won't have a big impact on the speed of the formula. Then, the iterated section is repeated over and over until 1) z escapes the bailout circle, 2) maximum number of iterations is reached, or 3) periodicity in the orbit of z is detected. In each iteration, a new value for z is found using the *current* value of z as one of the terms of the calculation, and this new value is then assigned to z. At the beginning of CGNewtonSinExp, Fractint is told to use the "xaxis" symmetry-drawing technique. After the location of the zoom-box corners has been noted, the parser then reads the initialization section one time per pixel. For this formula, that section is extremely simple. Now the parser moves to the iterated section. It performs all of the specified calculations that follow the colon, and then performs the bailout test. If the test evaluates to TRUE, and the other bailout conditions are not met, the parser loops back to the colon and starts calculating again. Since the bulk of the calculating takes place within the iterated loop, an iteration of CGNewtonSinExp will take longer than an iteration of Cardioid. I don't see how that could be avoided in this case, but as I'll show you later, you can often speed up a formula by putting as much calculating as possible in the initialization section rather than in the iterated loop. =================================== 9.0 APPROACHES TO WRITING FORMULAS =================================== At this point, we've covered the essentials you'll need to begin writing formulas. You now know the elements most commonly used in formulas, and you know some basic rules that govern how those elements are combined. But even though you have been shown the parts, we haven't really discussed how to go about actually writing a formula. Just what is the process? As you would probably guess, there are many different approaches available. The approach you choose will depend on your temperament and your mathematical abilities. The following list is certainly not exhaustive, but it may give you some ideas on how *you* might get started. 9.1 USING MATHEMATICAL INSIGHTS -------------------------------- Some people have so deep an understanding of the mathematics of fractals that they can use their insights to discover new fractals. Benoit Mandelbrot, for instance, understood the mathematics of Julia sets well enough to envision a new fractal that would serve as a "catalog" of all Julias. This new fractal is, of course, the Mandelbrot set. Few of us have this sort of deep insight, unfortunately. 9.2 ADAPTING AN EXISTING ALGORITHM ----------------------------------- Some beautiful fractals were found by investigating mathematical procedures developed for other purposes. For instance, in Fractint there are several built-in fractals and formulas for "Newton" fractals. These are based on a mathematical algorithm invented by Sir Isaac Newton for finding the roots of numbers. Surely he didn't have fractals in mind when he invented his method, but there they are! The "Halley" types are based on adaptations of another method for finding roots. If you know of an interesting algorithm, you might want to try adapting it to the formula format to see if there are any fractals lurking within. Later, in the discussion of "Using Values From Other Iterations", I'll give another example. 9.3 MUTATING AN EXISTING FORMULA --------------------------------- This is probably the easiest way to get started and get good results quickly. Look at the following formula: Mutantbrot { ;A mutation of the classic Mandelbrot set z = 0, c = pixel: ;standard initialization section z = z*z + c + sin(z) ;mutated iterated section |z| < 4 ;standard bailout test } All I did was to take the classic Mandelbrot formula and add a new term to the iterated section: "+ sin(z)". I didn't have any particular insight that led me to do this, I just tried it to see what would happen. The result is certainly different from the M-set, but interesting. One very good way to mutate a formula is to replace hard-coded functions with user-selectable functions. This is called "generalizing" the formula. For example, if a formula uses the expression "c = sqr(pixel)", you could change it to read "c = fn1(pixel)", and then experiment with different functions. Remember that just one generalized function will save you from the chore of typing dozens of hard-coded varients! You can also try enclosing key terms within functions. In a Mandelbrot formula, you could replace "z = z*z + c" with "z = fn1(z*z) + c". Now you can see the normal Mandelbrot by setting fn1() to IDENT, but you can also get interesting results with other functions. More examples of formulas created with the "mutation" approach can be found in my file FUBAR.FRM. 9.4 THE MONKEY-AT-THE-TYPEWRITER APPROACH ------------------------------------------ This is a reference to the old claim that if you put an infinite number of monkeys in front of typewriters and let them pound on the keys for an infinite length of time, eventually one would produce one of Shakespeare's sonnets. In this context, it means "just load your text editor and start typing". You know basically what a formula should look like, and you've seen lots of different "parts" used in other formulas, so play mad scientist. (Dr. Fractalstein?) Graft together different parts, add new ones of your own invention, and then see what happens. This approach is suitable for those of us (myself included) who are unencumbered by real math skills or insights but who want to play anyway! It often takes a lot of patience to find something interesting, but when you do it can be very exciting. It's hard to describe the experience of writing a more-or-less random formula and seeing incredible order and complexity in the emerging image. I find it both exhilarating and spooky. =========== 10.0 STYLE =========== If you have a collection of formula files, look them over with your text editor. You may notice that different authors can give their formulas very different appearances. Often, these visual differences are simply differences in "style". (The formulas in FRACTINT.FRM have been edited to have a consistent style, but other formulas vary.) Some authors like to put as much as possible on a single line, using commas to separate the different expressions, while other authors prefer to use a single expression per line. Some formulas have explanatory comments, while other formulas have none. Some authors use spaces between variables and operators, while other authors run them together. And some authors use indentation to make the different sections of their formulas more easily identifiable, while other authors do not. These differences demonstrate the personal preferences of the individual authors; the style that you use is entirely up to you. However, I have a few biases of my own that I'd like to inflict upon you. First, pick a style that makes sense to you, and then try to be consistent with it. This makes life easier for someone who is reading your formulas and trying to make sense of them. If you don't care about the comfort of others, remember that this unfortunate reader may be you, later on! Second, strive for clarity. This can be achieved in different ways: adding comments, using spaces wisely, indenting, choosing informative variable names, etc. Often the addition of parentheses can help make the logical grouping of formula elements more visible, even when the parentheses are not strictly necessary. ================ 11.0 TECHNIQUES ================ Okay, you know the basics. You've written some formulas of your own, and the rules about formula structure are becoming second nature. Now let's talk about techniques you can use to improve the performance of your formulas, or to make them do fancy new tricks. Warning: If you haven't already written several working formulas of your own, and aren't comfortable with the preceding information, you might want to come back to the rest of this document later. From this point on, we are venturing into material that is more complicated and subtle. 11.1 SPEED-UPS --------------- Here are some ways to make your formulas run a little faster. [11.1.1 AVOID EXPONENTIATION AND FUNCTION CALLS] There is often more than one way to accomplish a goal. If you'll compare the Mandelbrot, frm-A and frm-C1 formulas, you'll see that I used three different techniques to find the square of z: z*z, z^2, and sqr(z). All give the same answer but the first method, multiplication, is the fastest. As a rule, functions are slower than arithmetic operators, and exponents are typically slower than functions. If you need a function *other* than sqr(), though, you usually won't have a simple alternative. And sometimes you simply can't avoid using an exponent, but you are smart to look for situations where you can. [11.1.2 AVOID UNNECESSARY CALCULATIONS] Consider the following: speed-A { ;Demonstrates potential for speed-up z = 0: z = z*z + sin(pixel) |z| < 4 } speed-B { ;variation of speed-A showing one speed-up technique z = 0, sinp = sin(pixel): z = z*z + sinp |z| < 4 } Both formulas will result in exactly the same image being drawn, but speed-B is much faster. Why? In speed-A, the value of sin(pixel) must be calculated *once per iteration* while in speed-B it is only calculated *once per pixel*. We can do this because the value of pixel doesn't change during the iterated section. The value of z keeps changing from one iteration to the next, however, so we can't use the same trick with that variable. On my home computer (and at my preferred resolution) I can generate the speed-A fractal in about 82 seconds, while speed-B takes only 38 seconds. Speed-A may seem a little more straightforward, but your speed-hungry users will usually prefer using speed-B! [11.1.3 AVOID UNNECESSARY ITERATIONS] Compare the initialization section of frm-A to those in Mandelbrot and frm-C1. In the latter two, z is initialized with the value 0 (the traditional approach), while in frm-A it gets the value of pixel. Why the difference? Bert Tyler, the original Fractint programmer, recognized that the first iteration of the traditional Mandelbrot formula accomplishes nothing more than giving z the value of pixel -- step through the logic to see for yourself. So when he wrote the code for type "Mandel", he decided to "save" that iteration by initializing z to pixel. I have used the same trick in frm-A. It is a small speed-up, to be sure, but you may find it irresistible! If you carefully think your formulas through, you may find other speed-ups are possible. 11.2 SIMULATING THE IF..THEN CONSTRUCT --------------------------------------- Here's an summary of what I've learned about including conditional logic in a formula, and some warnings about possible pitfalls. [11.2.1 HOW IT WORKS] As I have mentioned, a formula is a program; the Fractint formula parser actually interprets a little programming language. Users who are familiar with programming languages like BASIC, C, or Pascal may find themselves wishing for some of the features of those languages. One of the most useful of those features is conditional logic, as used in the IF..THEN construct. Luckily for us, it is possible to simulate this construct with the parser language, even though it isn't explicitly included. Let's look at an example. Suppose you want to include the following logic in your formula: if A is negative then it gets the value of X, otherwise it gets the value of Y. In the C language, you could write this as: if (a < 0) a = x; else a = y; You could simulate this in a Fractint formula like this: neg = x * (a < 0) pos = y * (a >= 0) a = neg + pos or: a = (x * (a < 0)) + (y * (a >= 0)) Now both of those parser versions are quite a bit more obscure than the C version, and C has a reputation as an obscure language! But let's examine the first parser version and see how it works. It's time to return to the observation that a comparison will always evaluate to either TRUE or FALSE. That seems glaringly obvious on one level, but it's the details under the surface that make this technique work. The key detail is that Fractint appears to represent TRUE with a one, and FALSE with a zero. So suppose that a = (1.5,2) and let's see what happens with an expression like "neg = x * (a < 0)". Since this is an assignment statement, the parser will first have to evaluate what is on the right side of the '=', so it will know what to put into neg. When the parser comes to the comparison expression, it will give the answer FALSE, because the real part of a is 1.5. Since the parser represents FALSE with a zero, the expression simplifies to "x * 0", therefore neg = 0. Using similar logic, you should be able to prove that pos will end up equalling y. Since any single comparison is either TRUE or FALSE but not both, and because of the way I set up those comparisons, you should see that when a is negative, a = x + 0, and that when it is zero or positive, a = 0 + y. The second parser version is simply a condensation of the above, without the intermediate variables neg and pos. Which parser version do you prefer? [11.2.2 PITFALLS] For this technique to work properly there are some pitfalls to avoid, so I'll describe a few that can cause problems. As you read this section, you'll learn how *not* to set up conditional logic, and you'll also get good practice in analyzing formulas to see how they work. PITFALL 1: The order of the expressions can make a difference. The order in which expressions are evaluated can be important in *any* part of a formula. See section 7.2.10, for instance, for a discussion of how the rules of precedence affect the results of computations. But in the context of conditional logic, this principle can take on extra subtlety. Chuck Ebbert is the fellow who wrote the fast new version of the parser that was introduced with version 18, so we may regard him as a real authority. In his formula file BUILTN.FRM, Chuck suggested putting the comparison *after* the multiply when using the IF..THEN trick. He advised doing it for speed gains, but it can also affect the images that some formulas produce. Here's one such situation. Suppose you want to use the following algorithm in your iterated section: If 'z' (or more precisely, the *real* part of 'z') is negative, then z = fn1(z) + c ; otherwise, z = fn2(z) + c. Don't be tempted to set things up like this: IfThen-A1 { ;Demonstrates that the order of expressions can make a ;difference. In this example, the assignment is performed ;BEFORE the comparison. z = c = pixel: (z < 0) * (z = fn1(z) + c) (0 <= z) * (z = fn2(z) + c) |z| < 4 } The comparison expressions precede the assignment expressions as you read from left to right, but it appears that the parser actually evaluates the right-hand expression (the assignment) first. You can confirm this by looking at the images produced by this formula: IfThen-A2 { ;Functional equivalent of IfThen-A1 z = c = pixel: z = fn1(z) + c z = fn2(z) + c |z| < 4 } Since both formulas produce the same images, I conclude that the comparisons are, in effect, being ignored. If you wish, you can further simplify the formula: IfThen-A3 { ;Another equivalent of IfThen-A1 z = c = pixel: z = fn2(fn1(z) + c) + c |z| < 4 } While the preceding three formulas were instructive, they didn't do what we set out to do. Let's try again: IfThen-B1 { ;In this formula, the comparison is performed BEFORE the ;assignment, but there's still a subtle flaw. z = c = pixel: (z = fn1(z) + c) * (z < 0) (z = fn2(z) + c) * (0 <= z) |z| < 4 } This formula reverses the order of the comparison and assignment expressions. If you'll compare its images to those of IfThen-A1, you'll see that rearranging the expressions also changes the images. PITFALL 2: Don't try to embed an assignment statement within a larger expression. Okay, we've reordered the expressions in IfThen-B1 so the comparison is evaluated first. Because the assignment statements are within parentheses, it might seem reasonable to assume that an assignment will only occur if the comparison is TRUE. Certainly, that is our intent. Unfortunately, it isn't so. Look: IfThen-B2 { ;Functional equivalent of IfThen-B1 z = c = pixel: z = (fn1(z) + c) * (z < 0) ;line A z = (fn2(z) + c) * (0 <= z) ;line B |z| < 4 } Since IfThen-B2 produces the same images as IfThen-B1, we may assume that they are functionally equivalent. But in IfThen-B2 it is clear that *some* assignment always takes place, whether the comparison is TRUE or FALSE. This may well produce interesting results, but it isn't what we wanted. Remember that we wanted 'z' to get 'c' plus EITHER fn1(z) OR fn2(z). Putting the assignment within the parentheses didn't help us achieve conditional execution. In the Fractint parser language (as I understand it) there's no good reason to put an assignment statement *within* a larger expression, as we did in IfThen-A1 and IfThen-B1. That sort of thing may be useful in C and other languages, but in a Fractint formula it is likely to mislead you and your readers as to what is really happening. PITFALL 3: If you are trying to create an EITHER/OR choice, construct your formula carefully to ensure that the choices are mutually exclusive. Let's walk through an example for IfThen-B2. Suppose z < 0; then when line A is performed 'z' will get fn1(z) + c. Depending on the function selected for fn1() and the value of 'c', this *new* value of 'z' could be either negative, positive or zero, and it will determine whether the comparison in line B is TRUE or FALSE. In fact, for any particular iteration, the comparisons in lines A and B could be TRUE and TRUE, TRUE and FALSE, or FALSE and TRUE. This is getting very complicated! Comparisons line A line B Equivalent Expression --------------------------------------- TRUE / TRUE z = fn2(fn1(z) + c) + c TRUE / FALSE z = 0 FALSE / TRUE z = fn2(0) + c (Why no "FALSE / FALSE"? If line A is FALSE, then 'z' gets zero. Because of the way we wrote the comparison in line B, this would make line B necessarily TRUE...) Let's recap: In IfThen-A1 each comparison was ignored because the assignment had already been made before the comparison occurred. In IfThen-B1 the comparisons are not ignored, but our flawed logic causes the value of 'z' to change between the first comparison and the second. Got a headache yet? Try these on for size: IfThen-C1 { ;What we REALLY had in mind. z = c = pixel: neg = fn1(z) * (z < 0) pos = fn2(z) * (0 <= z) z = neg + pos + c |z| < 4 } IfThen-C2 { ;An alternate version of IfThen-C1 z = c = pixel: z = (fn1(z) * (z < 0)) + (fn2(z) * (0 <= z)) + c |z| < 4 } Here we finally have the algorithm we intended to implement. We made sure that the comparisons are evaluated before the assignments. We also took care to make the comparisons independent of each other. The point of the whole ugly exercise is this: unless you are careful, you can write a formula that operates in ways that you didn't intend. Before I leave this subject, let me address a possible objection. It may appear that if you successfully avoid pitfall #2 (embedding an assignment within a larger expression) then pitfall #1 (order of expressions) is a non-issue. I agree that in *most* cases it will not make a visible difference. IfThen-C1 appears to give the same images whether we write "neg = fn1(z) * (z < 0)" or "neg = (z < 0) * fn1(z)", for example. But putting the comparison after the multiply does seem to speed up many formulas, and as you'll see when you read section 12.4, "A Ghost Story", there *are* rare occasions when the order of the sub-expressions appears to affect the image, even though this may seem illogical. You are free to do as you like, of course, but I plan to follow Chuck's advice until I see a good reason to ignore it. 11.3 SETTING DEFAULTS ---------------------- Suppose that you have written a formula, and now you need to provide a bailout test. Even if you are just creating a normal escape-time formula, there are at least three different approaches to choose from. These different approaches are illustrated by the following three formulas. Each of these techniques has advantages, and I have used them all at various times. bailout-A { ;Hard coded bailout value ;p1 = parameter (default 0,0) z = pixel, c = fn1(pixel): z = fn2(z*z) + c + p1 |z| < 4 } bailout-B { ;Variable default -- additive ;p1 = parameter (default 0,0) ;p2 = bailout adjustment value (default 0,0) test = (4 + p2) z = pixel, c = fn1(pixel): z = fn2(z*z) + c + p1 |z| < test } bailout-C { ;Variable default -- conditional logic ;This formula requires floating-point ;p1 = parameter (default 0,0) ;p2 = bailout (default 4,0) ;The following line sets test = 4 if real(p2) = 0, else test = p2 test = (4 * (p2 <= 0)) + (p2 * (0 < p2)) z = pixel, c = fn1(pixel): z = fn2(z*z) + c + p1 |z| < test } Before we discuss the different approaches, examine the formula for a moment. It is a hybrid of a Mandelbrot and a Julia formula, with a couple of user-selectable functions thrown in for fun. P1 is used as a constant to be added with each iteration, and in the second and third formulas P2 is used to determine the bailout value. Now let's look at the bailout test in "bailout-A". Hard coding a value, as in "|z| < 4", is easiest to code and easiest to understand. It also will run the fastest. But it doesn't allow the user to change the value without editing the formula. Since I have added variable functions and a user parameter (p1), this value might not be the best choice for all situations. (If the user wants to use the "biomorph" option, a variable bailout test will also be desirable because this option generally works best with a high bailout value.) In "bailout-B", I have addressed this potential problem. Here, the value the user gives to P2 is added to 4 and stored in a variable called test. This variable is then referred to in the bailout test. If you decide that 3 would be a better bailout value than 4, you can give the real part of P2 the value -1, for example. As I noted in the section on speed-up techniques, putting the calculation of "4 + p2" in the initialization section will make the formula go faster than if the bailout test was written as "|z| < (4 + p2)". I didn't just set the bailout equal to p2, however, because then if the user left p2 at (0,0) the resulting image would be a blank screen. It is likely that a beginner or casual user of your formula would do just that, and would probably decide that your formula is defective! If you need to allow the user to vary some value, it is best (in my opinion) to be sure that the default values produce *some* image. This technique is still quite easy to implement. It has the drawback of being more obscure than hard coding, however. If you want the bailout value to be 16, for instance, you must understand the formula well enough to know that p2 must equal 12. The third technique is illustrated by "bailout-C". I first saw this trick used in Chuck Ebbert's formula file that I referred to earlier. In this approach, conditional logic is used. If the real part of p2 is left at zero (or is negative) then "test" is given the value 4. Otherwise, "test" gets the value of p2. In practice, this approach works more intuitively. If you want the bailout test to be 16, you set p2 to 16. There is a price to be paid for this intuitive operation, however: it's a little harder to write, and since it makes the formula more complicated, the formula will be a tiny bit slower. 11.4 USING VALUES FROM PREVIOUS ITERATIONS ------------------------------------------- In section 9.2 I talked about adapting an existing algorithm to the formula format. Here's an example of what I meant. Do you recognize the following series of numbers? What should the next number be? 1, 1, 2, 3, 5, 8, 13, 21 ... This is called the Fibonacci series. Each new number in the series is the sum of the previous two, so the next number should be 13 + 21, or 34. Let's see if we can make a formula out of this series. We might begin with the observation that in the Mandelbrot formula each new generation of 'z' is based on the previous generation. Maybe we can adapt the Mandelbrot formula for our purpose, but we want to involve the previous *two* generations. To do this, we'll need to be able to store old values for 'z'. Here's one way to do it: fibo-A { ;Derived from the Fibonacci series z = oldz = c = pixel: temp = z z = z * oldz + c oldz = temp |z| < 4 } Let's step through it. In the following discussion, I'm going to use the notation z(n) to refer to the value of z at the *end* of iteration number n, so z(3) would be the value of z at the end of the third iteration. First we initialize our variables including a new one, 'oldz', to the value of 'pixel'. Since we haven't completed an iteration yet, let's say z is at generation zero. Then the first line of our iterated section introduces another new variable, 'temp', which gets the value of z(0). Next we calculate a value for z(1): z(0) * oldz + c. Then oldz gets the value of temp, which is z(0). When we started into the iterated section, z, oldz and c all had the same value. Now z almost certainly has a different value, while oldz and c still have the value 'pixel'. Let's assume that the bailout conditions have not been met, and loop back for another iteration. Temp gets the value of z(1). Then we calculate z(2), and oldz gets the value of z(1) from temp. Now until we stop iterating: z(n) = z(n-1) * z(n-2) + c What have we got here? It's not quite the same as the Mandelbrot algorithm, and it's not quite the same as the Fibonacci algorithm either. Instead, we have a hybrid of the two. The formula makes an interesting image, but doesn't allow the user any room to play, so let's add a variable function: fibo-B { z = oldz = c = pixel: temp = z z = fn1(z * oldz) + c oldz = temp |z| < 4 } Now you can reproduce the fractal created by fibo-A by setting FN1 to IDENT, but you can also play with other functions. Keeping track of old values can be useful in other ways too. Look at the following formula, an example of the "in-and-out" formulas found in my file INANDOUT.FRM: inandout01 { ;Bradley Beacham [74223,2745] ;p1 = Parameter (default 0), real(p2) = Bailout (default 4) ;The next line sets test=4 if real(p2)<=0, else test=real(p2) test = (4 * (real(p2)<=0) + real(p2) * (0 key, and then enter "periodicity=0".) Now reflect that this is a very simple example. Can you invent more elaborate and interesting formulas that use the iteration counter idea? ============== 12.0 PROBLEMS ============== We've already looked at some of the possible pitfalls when you use the conditional logic (if..then) technique. Now let's look at a few other tricky areas. 12.1 POTENTIAL PROBLEMS WITH SYMMETRY -------------------------------------- Fractint's symmetry-drawing techniques can be a big time saver if used correctly. If you *know* that the fractals produced by a formula will always be symmetrical, then it is smart to declare the symmetry. But what if you are wrong? For better or worse, Fractint will attempt to follow your instructions anyway. Let's look at an example. sym-A { ;Non-symmetrical fractal z = c = pixel, k = (2.5,0.5): z = z^k + c |z| < 4 } sym-B (xaxis) { ;Sym-A with symmetry declared in error z = c = pixel, k = (2.5,0.5): z = z^k + c |z| < 4 } To see what I'm talking about, first load the sym-A formula and look at the image it makes. Clearly, the fractal is asymmetrical. Now look at the image produced by sym-B. The only difference between the formulas is the symmetry declaration, which doesn't really change the nature of the mathematical object described by the formula. We told Fractint to take a shortcut when drawing the image, so it did. You may even prefer the symmetrical image, but the truth remains that it is just an illusion. If you will rotate the zoom box slightly on sym-B, Fractint will stop trying to use the symmetry-drawing techniques, and the real fractal will come out. I pointed out earlier that the formula CGNewtonSinExp specifies xaxis symmetry. This is unfortunate, in my opinion, because if the imaginary part of p1 is anything other than zero, that fractal appears to lose its symmetry. Again, this can be demonstrated by rotating the zoom box. My point is this: Please be *sure* that your formula produces only symmetrical images before including a symmetry declaration. Otherwise you are likely to provoke confusion in your less experienced users, and to trigger misguided complaints about "Fractint bugs". Luckily, there are some ways to test for symmetry problems. First, make sure that the formula you are testing *does not* have a symmetry declaration. Now load the formula into Fractint and try this: 1) If you can enter values into the formula via the P1, P2 or P3 variables, try using a non-zero value for the imaginary parts. 2) If you can specify functions via FN1 ... FN4, try using FLIP as one or more of those functions. Use various combinations if possible. If you do these things and *still* only get symmetrical images, you are probably home free. But be sure you try a lot of different parameters before drawing your conclusions. 12.2 UNPARSABLE EXPRESSIONS IGNORED ------------------------------------ The formula parser will tell you about some kinds of errors when it finds them. If you try to use the expression "z = (z*z + (c*z)", it will recognize that there is a mismatch in the number of parentheses, for example. A different type of error goes unreported, though. Look at the following formulas: frm-D1 { ;Unparsable expression ignored z = c = pixel: z = z*z + sin z + c |z| < 4 } frm-D2 { ;fixed version of frm-D1 z = c = pixel: z = z*z + sin(z) + c |z| < 4 } In frm-D1 there is an incorrectly written expression, "sin z". Although the intent may be clear to you or me, the parser can't make proper sense of it. (Computers tend to take things *very* literally.) Since the formula produces a normal-looking Mandelbrot set, it appears to me that the parser is simply skipping over this part of the formula. Frm-D2 is here to show you what the fractal would look like if frm-D1 had been written correctly. 12.3 PATHOLOGICAL FORMULAS --------------------------- Some formula writers ignore (or are unaware of) the conventions, and produce formulas that "work" for the wrong reasons. For instance, take another look at IfThen-A1. This formula could be written with the best of intentions, and it may even produce an interesting fractal, but it is based on a misunderstanding. If you were the author of IfThen-A1, and if you understood the previous discussion about this formula, then I believe you should either simplify it as in IfThen-A2 or IfThen-A3, or correct it as in IfThen-C1 or IfThen-C2. In general, I think we should try to write formulas that someone else could comprehend. Even well-written formulas can be hard to understand, so let's not carelessly (or deliberately) make it worse! IfThen-A1 is obscure at best, and is likely to mislead the unwary reader. Even worse, some formula authors leave out parts. Consider: weirdo { ;Mandelbrot with no bailout test z = c = pixel: z = z*z + c } This one has an initializing section and an iterated section, but there's no bailout test. So what stops it from iterating? In my first analysis, I assumed it would stop iterating only when 1) the maximum number for iterations had been reached, or 2) 'z' fell into a periodic loop. Recall that in both of these situations, Fractint assumes that the "test point" (the value of pixel) is part of the set; therefore I expected that the whole screen would be colored blue. If you run the formula, though, you will see that I was wrong. I get what looks like a normal Mandelbrot lake, but the color bands outside the set are different. (Warning: if you're using Fractint version 18.2 or earlier and don't have an FPU or math coprocessor, the program might crash.) Luckily for me, Tim Wegner was able to explain what was happening here. In brief: The parser uses a stack to store values. (If you don't know what a stack is, just think of it for now as a location in memory.) After the iterated section has been performed, the value remaining on the stack determines whether the formula will be reiterated. It is just *assumed* that the last operation will be a comparison. Now if the value left on the stack is zero (as it would be with a FALSE comparison), then the iterations end. Otherwise (assuming we haven't reached the maximum iteration number or fallen into a periodic loop) the parser loops back up to the colon. In "weirdo", the value left on the stack would be the value assigned to 'z'. So if by chance real(z) == 0, the iterations would stop and the parser would choose the next test-point. For most test-points outside the M-set, however, the formula will keep iterating until 'z' becomes so large that a math error occurs; after this happens (and assuming Fractint didn't crash) the value left on the stack is zero. Fortunately, versions 19.0 and higher should be able to take these errors in stride and move on to the next test point, even without an FPU or coprocessor. As Tim put it, instead of using an escape-time algorithm, this formula could be said to use a "crash-and-burn-time algorithm". A formula that depends on math errors to produce an image! And "weirdo" is not unique; several formula files have been circulated containing formulas that lack bailout tests. I believe that this is undesirable, because even when it "works" the *way* that it works is exceedingly obscure. If this fails to persuade you, consider that when you create images that depend on computational quirks, math errors or bugs, you may find it impossible to reproduce your images later on when the software has been revised. On the other hand, Tim has pointed out that most real-life fractals are poorly understood (if at all), and that many beautiful and mysterious images have been produced that depend on computational quirks or inaccuracies. Why discount these images, just because we don't fully understand how they came about? What do you think? 12.4 A GHOST STORY ------------------- When formulas get complicated, it can be very difficult to understand what's happening. That comment just proves my keen grasp of the obvious, I suppose, but here's an interesting example I found recently. (Warning -- murky water ahead!) ghost { ;Demonstrates strange parser behavior ;To see effect, use floating point and make sure ;FN2() is not IDENT z = oldz = c1 = pixel, c2 = fn1(pixel) tgt = fn2(pixel), rt = real(tgt), it = imag(tgt): oldx = real(oldz) - rt oldy = imag(oldz) - it olddist = (oldx * oldx) + (oldy * oldy) x = real(z) - rt y = imag(z) - it dist = (x * x) + (y * y) a = (dist <= olddist) * (c1) b = (olddist < dist) * (c2) oldz = z z = z*z + a + b |z| <= 4 } ghostless-A { ;One solution to the ghost problem -- reorder expressions z = oldz = c1 = pixel, c2 = fn1(pixel) tgt = fn2(pixel), rt = real(tgt), it = imag(tgt): oldx = real(oldz) - rt oldy = imag(oldz) - it olddist = (oldx * oldx) + (oldy * oldy) x = real(z) - rt y = imag(z) - it dist = (x * x) + (y * y) a = (c1) * (dist <= olddist) ;Reverse order of value and comparison b = (c2) * (olddist < dist) ;Ditto oldz = z z = z*z + a + b |z| <= 4 } ghostless-B { ;Another solution to the ghost problem -- reinitialize z = oldz = c1 = pixel, c2 = fn1(pixel) tgt = fn2(pixel), rt = real(tgt), it = imag(tgt): oldx = real(oldz) - rt oldy = imag(oldz) - it olddist = (oldx * oldx) + (oldy * oldy) x = real(z) - rt y = imag(z) - it dist = (x * x) + (y * y) a = b = 0 ;Make sure a & b are set to zero a = (dist <= olddist) * (c1) b = (olddist < dist) * (c2) oldz = z z = z*z + a + b |z| <= 4 } First, look at the "ghost" formula. I created this formula while experimenting with new variations on the "in and out" theme. You won't need to try to follow all of its logic; just note that this is a fairly complicated formula, with several variables and functions, an iterated section with many steps, and a couple of lines that use the conditional logic technique. If you'll look at the images that the "ghost" formula makes (make sure you're using floating-point math) you should notice a very strange thing: it appears that there is more than one image, and that these images are somehow superimposed over each other. This is an interesting effect, but it is due to the parser behaving in a way that I hadn't intended. After quite a bit of experimenting, I found a few ways to make the ghosting effect go away. For example, set FN2() to IDENT. No more ghosting; any other function will make it reappear, however. Now try the same formula with integer math. Again, no ghosting problem, regardless of the function selected for FN2(). Turn floating point back on and look at the images made by "ghostless-A" and "ghostless-B". Once again, the ghosting effect vanishes. If you'll compare these formulas to "ghost", you should see that the only difference is in the way the conditional logic lines were written. Instead of writing "a = (dist <= olddist) * (c1)", I wrote "a = (c1) * (dist <= olddist)". Thus, the order of the expressions seems to make a difference, even when care is taken to make the choices mutually exclusive, and even when assignments are performed *after* the comparisons are made. A possible hint about what is happening is provided by "ghostless-B". This formula is just like "ghost", except for the addition of the line "a = b = 0" just before the lines with the conditional logic. With this line added, the formula makes the same images as "ghostless-A". The meaning of this (it seems to me) is that in the "ghost" formula, if "dist <= olddist" is FALSE, 'a' doesn't always get set to zero as I would expect. My guess is 'a' just keeps the old value from the previous iteration. It has been suggested that floating-point optimizations may be behind this odd behavior. If any reader knows more, please contact me. But in the meantime, this appears to reinforce Chuck Ebbert's suggestion that when using conditional logic we should write the comparison expression after the '*'. Finally, I should point out that this "ghosting" effect is quite unusual in my experience; it's *not* an everyday problem. I believe it is aggravated by the formula's complexity. Let me illustrate: ghostless-C { ;Yet another solution -- simplify! z = c1 = pixel, c2 = fn1(pixel), olddist = 100 tgt = fn2(pixel), rt = real(tgt), it = imag(tgt): x = real(z) - rt y = imag(z) - it dist = (x * x) + (y * y) a = (dist <= olddist) * (c1) b = (olddist < dist) * (c2) olddist = dist z = z*z + a + b |z| <= 4 } I simplified the logic of "ghost" and by doing this, I was able to eliminate three variables: oldx, oldy, and oldz. This formula has no ghosting problem, even though the conditional statements are the same as in "ghost". And because it is simpler, it is also faster! So here we have a problem with five different solutions: setting FN2() to IDENT, using integer math, changing the order of expressions, re-initializing variables, and simplifying formula logic. Very peculiar... Although I don't yet know the real cause of the problem, I have drawn some lessons from the experience: If a formula gets too complicated, things might go haywire. When the parser starts behaving strangely, try rewriting the formula in a different way. And above all, look for ways to simplify your formulas. =========================== 13.0 WHERE TO GO FROM HERE =========================== One of the amazing things about exploring a fractal is that you can continue to zoom deeper and deeper into the image, and yet you keep seeing new details. Similarly, the study of fractals and formulas appears to offer an inexhaustible supply of new things to learn. This document has only scratched the surface. Here are a few suggestions on how you can increase your knowledge. 13.1 LEARN MORE ABOUT COMPLEX NUMBERS -------------------------------------- Complex math lies at the heart of the fractals produced by the parser. If you have no understanding of the math, I don't see how you can really understand a formula. So find a good teacher and take a class. Read a textbook or FRACTAL CREATIONS. Practice complex arithmetic with a paper and pencil. But don't skip over the subject, and don't be overwhelmed by the word "complex"; if you can learn algebra, you can learn about complex numbers. 13.2 LEARN MORE ABOUT PROGRAMMING ---------------------------------- It bears repeating that a formula is a program. The parser language, although comparatively small, is sometimes more obscure than most modern programming languages; so if you have never learned another programming language, doing so would probably be very helpful to you. Although the details vary from one language to another, there are common concepts that will help you understand Fractint formulas. If you become familiar with the main concepts (variables, looping, assignment and comparison, conditional execution, etc.) in a setting that makes these concepts easier to understand, that experience will help you understand the programming aspects of a formula. 13.3 LEARN MORE ABOUT FRACTALS ------------------------------- You may not find this subject taught in your school yet, but there are some excellent books that can help you learn more. The best introductory book for a Fractint user (in my opinion) is still FRACTAL CREATIONS by Timothy Wegner and Bert Tyler. The second edition of the book also includes a CD with some terrific images. James Gleick's CHAOS: MAKING A NEW SCIENCE is a fascinating introduction to the related notions of chaos, non-linear dynamics and fractals. It discusses how and why these concepts came into being, and gives glimpses into the minds of some fascinating people. CHAOS AND FRACTALS: NEW FRONTIERS OF SCIENCE by Peitgen, Jurgens and Saupe is practically an encyclopedia of fractals; it's full of history, good illustrations, illuminating discussions of the math, and sample programs. THE FRACTAL GEOMETRY OF NATURE by Benoit Mandelbrot is the book that started it all, but be aware that it is *not* for the mathematical dilettante; neither is FRACTALS EVERYWHERE by Michael Barnsley. Both books are very highly regarded as standard works in the fractal library, however. Clifford Pickover has written several books that discuss fractals, including COMPUTERS, PATTERN, CHAOS AND BEAUTY and MAZES FOR THE MIND. These books are jammed with great pictures, stimulating ideas, program listings and pseudo-code, and much more. Dr. Pickover also edits the journal COMPUTERS AND GRAPHICS, which includes articles on fractal graphics. There are a few other periodicals that deal with fractals. AMYGDALA is published in the US, while the diskette-based FRAC'CETERA comes to us from the UK. Both of these publications are warmly endorsed by their readers. 13.4 FIND OTHER FRACTAL ENTHUSIASTS ------------------------------------ When you're struggling to learn new concepts, it's wonderful to have a knowledgeable friend who'll help you. If you don't know anyone who fits that description, get a modem and go online. You'll be able to meet fellow fractal nuts on CompuServe, for example, in the GRAPHDEV area. Look for fractal discussions on the Internet or on your favorite online service or BBS, and if nobody is talking about the subject, bring it up yourself. We're a small minority, but our ranks are constantly growing. For years I created fractal images by and for myself, and I'm here to tell you that it's a *lot* more fun when you meet people with whom you can share your questions and accomplishments. ================ 14.0 CONCLUSION ================ That's all for this iteration of the file. I hope you found it to be interesting and instructive. There's no doubt in my mind that it can be improved. If you have any insights or suggestions that will make it better, please share them with me so I can share them with everyone else.