home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Hot Shareware 32
/
hot34.iso
/
ficheros
/
DGRAF
/
FRAIN196.ZIP
/
FRMTUT.ZIP
/
FRMTUTOR.TXT
< prev
Wrap
Text File
|
1995-02-24
|
115KB
|
2,389 lines
*******************************
* *
* 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 <F6> 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 <Z> 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 <F> 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 <F> 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 <X> 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 <Z> 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 <Z> 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 <Z> 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<p2))
z = oldz = pixel, c1 = fn1(pixel), c2 = fn2(pixel):
a = (|z| <= |oldz|) * (c1) ;IN
b = (|oldz| < |z|) * (c2) ;OUT
oldz = z
z = fn3(z*z) + a + b + p1
|z| <= test
}
Here's the basic idea behind the "in-and-out" algorithm: Did the last
iteration move z closer to the origin (0,0) or farther away from it? If
closer, use one value. If farther away, use a different value. Picture
the complex plane with the bailout circle centered on the origin. In my
imagination this resembled a radar screen, with z moving about within
the circle. Is z getting closer to the center, or farther away?
To answer that question, I kept track of the value of z from the
previous iteration by storing it in a variable called oldz, and then
compared the values of |z| and |oldz|.
If you feel bewildered, hold on. Let's look at the formula in detail,
step by step, to illustrate how it works. To make it a little easier, I
annotated the formula with additional comments, and put spaces between
different functional parts to make them easier to see.
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<p2))
;initialize other variables
z = oldz = pixel, c1 = fn1(pixel), c2 = fn2(pixel):
;did previous iteration move z in or out?
a = (|z| <= |oldz|) * (c1) ;IN
b = (|oldz| < |z|) * (c2) ;OUT
;save value of z before changing it
oldz = z
;calculate new z
z = fn3(z*z) + a + b + p1
;bailout test
|z| <= test
}
First comes the formula name. Next is a comment that tells you how the
user variables P1 and P2 are used. Then we give the variable 'test' a
value -- this will be used in the bailout test -- and we set up some
other variables. I call my main variable 'z', and set it equal to
pixel. I also create a variable called 'oldz' and set it to the value of
pixel. Then I create two other variables called 'c1' and 'c2'. Recall
that the general algorithm is to use one value if z moves closer to the
origin, and another value if z moves away. To accomplish this, I opted
to use two different versions of c: c1 gets fn1(pixel) and c2 gets
fn2(pixel).
Since we just found the colon, that's the end of the initialization
section. Let's proceed to the iterated section. Remember that the main
question is "did z move closer to the origin, or farther away?" To
answer this question, we compare |z| to |oldz|. We have to account for
the possibility that they are the same, so the first comparison is
stated as "|z| <= |oldz|". On the first iteration 'oldz' was
initialized to equal 'z' so this will be the case, but on subsequent
iterations, who knows? At any rate, if this comparison is TRUE, the
statement has the value of 1, so 'a' gets the value of 'c1'; if it is
FALSE, 'a' gets the value 0.
A similar comparison covers the opposite possibility. If |oldz| is
less than |z|, then 'b' gets the value 'c2'; otherwise it gets 0.
Note that we made sure the possibilities are mutually exclusive. In
every case, if one comparison is TRUE then the other is FALSE, but the
TRUE comparison may change from iteration to iteration.
Then, we save the value of 'z' into 'oldz', just before we calculate a
new value for 'z'. After the first iteration it is likely that 'oldz'
and 'z' will have different values, but 'oldz' will always have the
value of 'z' from the previous iteration, as in the "fibo" formulas.
To calculate the new 'z', we use "z = fn3(z*z) + a + b + p1".
Because of the way we set up the comparisons, this will work out to mean
either "z = fn3(z*z) + c1 + 0 + p1" or "z = fn3(z*z) + 0 + c2 + p1".
(Speaking of the comparisons, you have probably noticed that I didn't
follow my own advice about writing the comparison after the multiply.
When I wrote that formula, I was unaware of the possible complications
involved. I'm still learning...)
Finally, we come to the relatively simple bailout test.
Whew, that was a bit of a workout! So what's the cosmic significance of
it all? Beats me! If it does nothing else, though, it should show you
that it is possible to invent fairly elaborate algorithms for your
formulas without any notions about their ultimate mathematical
significance. And if you're smart enough to see the significance of
things like this, then you're in a great position to exploit that
knowledge.
It should also demonstrate that saving values from past iterations can
be a useful technique!
11.5 DISSECTING A FORMULA WITH ALGEBRA
---------------------------------------
Fractint was designed to work with complex numbers. It has all kinds of
mathematical operators and functions available that were written with
complex math in mind.
But the fact that complex arithmetic works like algebra can be exploited
for insight and for interesting results. Let's take a closer look at
how complex multiplication is performed.
Remember that a complex number can be expressed as the combination of a
real and an imaginary part. The complex number x + yi, for example,
has a real part of x and an imaginary part of y, while 'i' represents
the square root of -1.
To find the square of the number, we can treat the complex number as an
ordinary algebra expression, with 'i' being a variable. This means that
(x + yi) * (x + yi) == (x^2 + 2xyi + yi^2). Now if you remember that
i^2 == -1, this can be simplified to ((x^2 - y^2) + 2xyi).
We can write a formula that recreates this process, doing complex
arithmetic the hard way. For want of a better term, I call this
"dissecting" the formula.
dissected-A { ;A dissected Mandelbrot
z = 0, c = pixel:
x = real(z), y = imag(z) ;isolate real and imaginary parts
newx = x*x - y*y ;calculate real part of z*z
newy = 2*x*y ;calculate imag part of z*z
z = newx + flip(newy) + c ;reassemble z
|z| < 4
}
In this formula, I isolate the real and imaginary parts of z, and then
perform the math described above. Let's suppose for a moment that
z = 2 + 3i, and step through the iterated section of the formula.
First, the real part of z is extracted by using the "real" function.
This result is put in a complex variable called 'x'. Then the imaginary
part of z is extracted using the "imag" function, and this is put into
'y'. Thus, x = (2,0) and y = (3,0).
Now, values for the real and imaginary parts of z*z are calculated by
emulating the algebraic analysis above. That is, we find the value of
x*x - y*y; that's the real part of z*z. Then we find the value of
2*x*y, which is the imaginary part of z*z.
Now we "reassemble" z using these values. Remember that when we used
the "imag" function and put the result in 'y', it went into the *real*
part of 'y'. This means that we have to convert the value in 'newy'
into an imaginary number. We do this with the "flip" function. The
"flip" function, in effect, turns a complex number upside down by
swapping the real and the imaginary values. Thus, flip(x + yi) returns
y + xi. If x = (2,0) and y = (3,0), then newy = (12,0). Therefore,
flip(newy) = (0,12). Finally we add c and perform the bailout test.
If you'll run this formula, you'll see that it does indeed produce the
Mandelbrot set. But that's a lot more work than the standard Mandelbrot
formula. Why bother?
The answer is that by "laying the parts out", so to speak, you can
perform operations on them that would be difficult to do with a formula
written in the normal way. Consider the next formula:
dissected-B { ;A mutation of "dissected-A"
z = 0, c = pixel, k = 2 + p1:
x = real(z), y = imag(z)
newx = fn1(x*x) - fn2(y*y)
newy = k*fn3(x*y)
z = newx + flip(newy) + c
|z| < 4
}
By experimenting with various values for fn1(), fn2(), fn3() and p1, you
can produce all sorts of interesting variants on the Mandelbrot set. I
suggest starting with all of the functions set at "ident" and p1 at 0,
and then changing one of them at a time.
Practice your complex arithmetic so it becomes easier to do, and you'll
find that it is possible to dissect many different formulas. See my
OVERKILL.FRM for some other examples.
11.6 USING A COUNTER
---------------------
Fractint has built-in mechanisms to count the iterations of our
formulas. Normally we just rely on the automatic counter, but sometimes
it's more fun to be abnormal. Here's a simple example of how you can
manually track the iteration count, to produce fractals that are
definitely out of the ordinary.
shifter { ;Use a counter to shift algorithms
z = c = pixel, iter = 1, shift = p1, test = 4 + p2:
lo = (z*z) * (iter <= shift)
hi = (z*z*z) * (shift < iter)
iter = iter + 1
z = lo + hi + c
|z| < test
}
Let's first look at the mechanics of the formula. Note that there is a
variable called 'iter', which is used to keep a running count of the
iterations. For each pixel it is initialized with 1, and in each pass
through the loop the value is incremented; so at the beginning of the
first iteration 'iter' will equal 1, on the second iteration it will
equal 2, etc.
Also note that the formula uses conditional logic to select between two
algorithms: do we calculate z*z or z*z*z? The question is settled by
comparing the iteration number to a variable called 'shift', which
equals p1. (I could have just used p1 in the formula, but I think using
a variable called 'shift' makes the meaning a little clearer.)
Suppose you set p1 to 75, and left the value for maximum iterations at
150. This means that for the first 75 iterations, the formula would
calculate z = z*z + c, but from iteration 76 to 150, it would calculate
z = z*z*z + c. In other words, it is shifting mid-stream from the
normal Mandelbrot algorithm to the "Cubic Mandelbrot" algorithm.
Set p1 to equal 0, and you'll see a Cubic Mandelbrot. Set it at 150 and
you'll see the normal Mandelbrot. But when you use a value in-between,
you'll get something much stranger. Usually it looks like a normal
Mandelbrot with strange growths inside its lake area. Weird! (Note:
this formula seems to work best with Fractint's periodicity logic turned
off. To do this, just hit the <G> 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.