Math Toolkit For Real-Time Programming TQW - Darksiderg PDF
Math Toolkit For Real-Time Programming TQW - Darksiderg PDF
Math Toolkit For Real-Time Programming TQW - Darksiderg PDF
Real-Time Development
Jack W. Crenshaw
CMP Books
Lawrence, Kansas 66046
CMP Books
CMP Media, Inc.
1601 W. 23rd Street, Suite 200
Lawrence, KS 66046
USA
Designations used by companies to distinguish their products are often claimed as trademarks. In
all instances where CMP Books is aware of a trademark claim, the product name appears in initial
capital letters, in all capital letters, or in accordance with the vendors capitalization preference.
Readers should contact the appropriate companies for more complete information on trademarks
and trademark registrations. All trademarks and registered trademarks in this book are the property of their respective holders.
Copyright 2000 by CMP Media, Inc., except where noted otherwise. Published by CMP Books,
CMP Media, Inc. All rights reserved. Printed in the United States of America. No part of this publication may be reproduced or distributed in any form or by any means, or stored in a database or
retrieval system, without the prior written permission of the publisher; with the exception that the
program listings may be entered, stored, and executed in a computer system, but they may not be
reproduced for publication.
The programs in this book are presented for instructional value. The programs have been carefully
tested, but are not guaranteed for any particular purpose. The publisher does not offer any warranties and does not guarantee the accuracy, adequacy, or completeness of any information herein
and is not responsible for any errors or omissions. The publisher assumes no liability for damages
resulting from the use of the information in this book or for any infringement of the intellectual
property rights of third parties that would result from the use of this information.
Cover art created by Janet Phares.
ISBN: 1-929629-09-5
Table of Contents
Preface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xi
Who This Book Is For . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xi
Computers are for Computing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xi
About This Book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xiv
About Programming Style. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .xv
On Readability . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xviii
About the Programs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xviii
About the Author . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xix
Section I
Foundations . . . . . . . . . . . . . . . . . . . . . . . . . 1
Chapter 1
Related Constants. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .6
Time Marches On. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .7
Does Anyone Do It Right? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .9
A C++ Solution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .12
Header File or Executable? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .14
Gilding the Lily. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .16
Chapter 2
iii
iv
Table of Contents
Whats Your Sign? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
The Modulo Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Chapter 3
Appropriate Responses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Belt and Suspenders . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
C++ Features and Frailties . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Is It Safe? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Taking Exception . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
The Functions that Time Forgot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Doing it in Four Quadrants. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Section II
27
28
28
31
33
35
37
Fundamental Functions . . . . . . . . . . . . . . 41
Chapter 4
Square Root . . . . . . . . . . . . . . . . . . . . . 43
48
49
57
58
62
63
65
66
68
68
69
70
72
74
78
83
84
85
86
87
Table of Contents
Chapter 5
Chapter 6
Arctangents: An AngleSpace
Odyssey . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
vi
Table of Contents
Chapter 7
Section III
Chapter 8
181
182
182
186
189
190
192
195
196
197
199
202
204
208
210
215
215
215
216
217
219
226
227
231
232
234
234
234
239
239
242
243
Table of Contents
vii
Chapter 9
Chapter 10
viii
Table of Contents
Interpolation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Paradise Regained . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
A Parting Gift . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
References . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Chapter 11
Chapter 12
323
325
328
331
333
334
335
336
337
339
340
343
346
346
348
349
350
351
354
The Concept . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Reducing Theory to Practice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
The Basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Howd I Do? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Whats Wrong with this Picture? . . . . . . . . . . . . . . . . . . . . . . . . . .
Home Improvements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
A Step Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Cleaning Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Generalizing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
The Birth of QUAD1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
The Simulation Compiler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Real-Time Sims . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Control Systems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Back to Work . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Printing the Rates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Higher Orders . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Affairs of State . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
355
358
359
362
363
364
365
366
368
369
370
371
372
372
374
376
379
Table of Contents
ix
Appendix A
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 457
Whats on the CD-ROM? . . . . . . . . . . . . . . . . . . . . . . 474
Table of Contents
Preface
Who This Book Is For
If you bought this book to learn about the latest methods for writing Java
applets or to learn how to write your own VBx controls, take it back and
get a refund. This book is not about Windows programming, at least not
yet. Of all the programmers in the world, the great majority seem to be writing programs that do need Java applets and VBx controls. This book, however, is for the rest of us: that tiny percentage who write software for
real-time, embedded systems. This is the software that keeps the world
working, the airplanes flying, the cars motoring, and all the other machinery
and electronic gadgets doing what they must do out in the real world.
xi
xvii
rest assured, a copy went along with me. I considered that library to be my
toolbox, and I also considered it my duty to supply my own tools, in the
same spirit that an auto mechanic or plumber is expected to bring his or her
tools to the job. Many of those routines continue to follow me around, even
today. The storage media are different, of course, and the languages have
changed, but the idea hasnt changed. Good ideas never grow old.
A few years ago, I was asked to teach a class in FORTRAN as an adjunct
professor. One look at the textbook that the school faculty had already chosen for the class told me that it was not a book I wanted to use. The examples were textbook examples right out of K & Ps other seminal book,
Elements of Programming Style, 2nd edition (McGraw-Hill, 1978), of how
not to write FORTRAN. As is the case with so many textbooks, each program was just that: a stand alone program (and not a well-written one at
that) for solving a particular problem. The issues of maintainability, reusability, modularity, etc., were not addressed at all. How could they be?
Functions and subroutines werent introduced until Chapter 9.
I decided to take a different tack, and taught the students how to write
functions and subroutines on day one. Then I assigned projects involving
small subroutines, beginning with trivial ones like abs, min, max, etc., and
working up to more complex ones. Each student was supposed to keep a
notebook containing the best solutions.
Each day, Id ask someone to present their solution. Then wed discuss
the issues until we arrived at what we considered to be the best (I had more
than one vote, of course), and that solution, not the students original ones,
was the one entered into everyones notebook. The end result was, as in K
& Ps case, twofold. Not only did the students learn good programming
practices like modularity and information hiding, they had a ready-made
library of top-quality, useful routines to take to their first jobs. I blatantly
admit that my goal was to create as many Crenshaw clones as possible. I
dont know how well the school appreciated my efforts I know they
didnt ask me back but the effect on the students had to be similar to the
one the judge gets when they instruct the jury to forget they heard a particularly damaging piece of testimony. Try as they might, I suspect those students are still thinking in terms of small, reusable subroutines and functions.
A little bit of Crenshaw now goes with each of them.
Do I hope to do the same with you? You bet I do. In giving you examples
of math algorithms, its inevitable that I show you a bit of the style I like to
use. Thats fine with me, and for good reason: the methods and styles have
been thoroughly proven over many decades of practical software applications. You may already have a style of your own, and dont want to change
xiii
xiv
Preface
lie hidden inside your CD disk player. The age of digital control is upon is. If
you doubt it, look under the hood of your car.
As a result of the history of computing, we now have two distinct disciplines: the non-numeric computing, which represents by far the great majority of all computer applications, and the numeric computing, used in
embedded systems. Most programmers do the first kind of programming,
but the need is great, and getting greater, for people who can do the second
kind. Its this second kind of programming that this book is all about. Ill be
talking about embedded systems and the software that makes them go.
Most embedded systems require at least a little math. Some require it in
great gobs of digital signal processing and numerical calculus. The thrust of
this book, then, is twofold: its about the software that goes into embedded
systems and the math that lies behind the software.
xv
are much more math-intensive than others, they are all aimed at the same
ultimate goal: to provide software tools to solve practical problems, mostly
in real-time applications. Because some algorithms need more math than
others, it may seem to you that math dominates some chapters, the software, others. But the general layout of each chapter is the same: First,
present the problem, then the math that leads to a solution, then the software solution. Youll need at least a rudimentary knowledge of math and
some simple arithmetic. But Ill be explaining the other concepts as I go.
Some readers may find the juxtaposition of theory and software confusing. Some may prefer to have the software collected all together into nice,
useful black boxes. For those folks, the software is available alone on
CD-ROM. However, I will caution you that you will be missing a lot, and
perhaps even misusing the software, if you dont read the explanations
accompanying the software in each chapter. Im a firm believer in the notion
that everything of any complexity needs a good explanation even a simple-minded, overly detailed one. Nobody ever insulted me by explaining the
obvious, but many have confused me by leaving things out. I find that most
folks appreciate my attempts to explain even the seemingly obvious. Like
me, theyd rather be told things they already know than not be told things
they dont know.
On each topic of this book, Ive worked hard to explain not only what
the algorithms do, but where they came from and why theyre written the
way they are. An old saying goes something like, give a man a fish, and
tomorrow hell be back for another; give him a fishing pole, teach him how
to use it, and he will be able to catch his own. Ive tried hard to teach my
readers how to fish. You can get canned software from dozens, if not thousands, of shrink-wrap software houses. Youll get precious little explanation
as to how it works. By contrast, if my approach works and you understand
my explanations, youll not only get software you can use, but youll also
know how to modify it for new applications or to write your own tools for
things Ive left out.
xvi
Preface
Needless to say, the style Im most likely to teach is the one I use. Over
the years, Ive learned a lot of things about how and how not to write software. Some of it came from textbooks, but most came from trial and error.
After some 40 years of writing software, I think Im finally beginning to get
the hang of it, and I think I have some things to teach.
In their seminal book, Software Tools, (Addison-Wesley, 1976) Brian
Kernighan and P.J. Plauger (K & P) suggest that one of the reasons that
some programmers and experienced ones, at that write bad programs
is because no ones ever showed them examples of good ones. They said,
We don't think that it is possible to learn to program well by reading platitudes about good programming. Instead, their approach was to show by
example, presenting useful software tools developed using good programming practices. They also presented quite a number of examples, taken from
programming textbooks, on how not to write software. They showed what
was wrong with these examples, and how to fix them. By doing so, they
killed two birds with one stone: they added tools to everyones toolboxes
and also taught good programming practices.
This book is offered in the same spirit. The subject matter is different
K & P concentrated mostly on UNIX-like text filters and text processing,
whereas well be talking about math algorithms and embedded software
but the principle is the same.
For those of you too young to remember, it may surprise you (or even
shock you) to hear that there was a time when calling a subroutine was considered poor programming practice it wasted clock cycles. I can remember being taken to task very strongly, by a professional FORTRAN
programmer who took great offense at my programming style. He saw my
heavy use of modularity as extravagantly wasteful. Hed intone, 180
microseconds per subroutine call. My response, I can wait, did not
amuse.
I was lucky; the fellow who taught me FORTRAN taught me how to
write subroutines before he taught me how to write main programs. I
learned modularity early on, because I couldnt do anything else.
In those days, I soon developed a library of functions for doing such
things as vector and matrix math, function root-solving, etc. This was back
when punched cards were the storage medium of choice, and the library
went into a convenient desk drawer as card decks, marked with names like
Vectors and Rotation. In a way, I guess these decks constituted the
original version of the Ada package.
That library followed me wherever I went. When I changed jobs, the
software Id developed for a given employer naturally stayed behind, but
xvii
rest assured, a copy went along with me. I considered that library to be my
toolbox, and I also considered it my duty to supply my own tools, in the
same spirit that an auto mechanic or plumber is expected to bring his or her
tools to the job. Many of those routines continue to follow me around, even
today. The storage media are different, of course, and the languages have
changed, but the idea hasnt changed. Good ideas never grow old.
A few years ago, I was asked to teach a class in FORTRAN as an adjunct
professor. One look at the textbook that the school faculty had already chosen for the class told me that it was not a book I wanted to use. The examples were textbook examples right out of K & Ps other seminal book,
Elements of Programming Style, 2nd edition (McGraw-Hill, 1978), of how
not to write FORTRAN. As is the case with so many textbooks, each program was just that: a stand alone program (and not a well-written one at
that) for solving a particular problem. The issues of maintainability, reusability, modularity, etc., were not addressed at all. How could they be?
Functions and subroutines werent introduced until Chapter 9.
I decided to take a different tack, and taught the students how to write
functions and subroutines on day one. Then I assigned projects involving
small subroutines, beginning with trivial ones like abs, min, max, etc., and
working up to more complex ones. Each student was supposed to keep a
notebook containing the best solutions.
Each day, Id ask someone to present their solution. Then wed discuss
the issues until we arrived at what we considered to be the best (I had more
than one vote, of course), and that solution, not the students original ones,
was the one entered into everyones notebook. The end result was, as in K
& Ps case, twofold. Not only did the students learn good programming
practices like modularity and information hiding, they had a ready-made
library of top-quality, useful routines to take to their first jobs. I blatantly
admit that my goal was to create as many Crenshaw clones as possible. I
dont know how well the school appreciated my efforts I know they
didnt ask me back but the effect on the students had to be similar to the
one the judge gets when they instruct the jury to forget they heard a particularly damaging piece of testimony. Try as they might, I suspect those students are still thinking in terms of small, reusable subroutines and functions.
A little bit of Crenshaw now goes with each of them.
Do I hope to do the same with you? You bet I do. In giving you examples
of math algorithms, its inevitable that I show you a bit of the style I like to
use. Thats fine with me, and for good reason: the methods and styles have
been thoroughly proven over many decades of practical software applications. You may already have a style of your own, and dont want to change
xviii
Preface
it. Thats fine with me also. Nobody wants to turn you all into Crenshaw
clones. If you take a look at my style, and decide yours is better, good for
you. We all have to adopt styles that work for us, and I dont really expect
the cloning to fully take. Nevertheless if, at the end of this book, I will have
given you some new ideas or new perspectives on programming style, I will
have done my job.
On Readability
One aspect of my style will, I hope, stand out early on: I tend to go for simplicity and readability as opposed to tricky code. Im a strong believer in the
KISS principle (Keep It Simple, Simon). When faced with the choice of using
a tricky but efficient method, or a straightforward but slower method, I will
almost always opt for simplicity over efficiency. After all, in these days of
800 MHz processors, I can always find a few more clock cycles, but programming time (and time spent fixing bugs in the tricky code) is harder to
come by. Many people talk about efficiency in terms of CPU clock cycles,
but unless Im coding for an application thats going to be extremely
time-critical, theres another thing I find far more important: the time it
takes to bring a program from concept to reality. To quote K & P again,
First make it run, then make it run faster.
A corollary to this concept requires that programs be written in small,
easily understandable and easily maintainable chunks. The importance of
the cost of program maintenance is only now becoming truly realized.
Unless you want to be maintaining your next program for the rest of your
life, picking through obscure branches and case statements, and wondering
why a certain construct is in there at all, youll take my advice and write
your programs, even embedded programs, in highly modular form.
xix
guage, though Pascal probably comes the closest. I find that the most rabid
partisans for a certain language are those who have never (or rarely) programmed in any other. Once youve programmed a certain number of languages, writing in one or the other is no big deal, and the debates as to the
pros and cons of each can become a little less heated. Ill be using C++ for
my examples for quite a practical reason: Its the language thats most popular at the moment.
At the same time, I must admit that this choice is not without its downside. At this writing, few real-time embedded systems are being written in
C++, and for good reason: the language is still in a state of flux, and some of
its more exotic features present significant risk of performance problems.
Over the last few years, C and C++ language expert P.J. Plauger has been
engaged in an effort to define a subset of C++ for embedded systems. At this
writing, that effort is complete; however, no major software vendor yet has
a compiler available for that standard.
xx
Preface
other as I was driving to and from work, and wed give each other a fellow-sports-car honk and wave.
I never really intended to be a computer programmer, much less a software engineer or computer scientist, terms that hadnt even been invented at
the time. My formal training is in Physics, and I joined NASA to help put
men on the Moon; as a sci-fi buff of long standing, I felt I could do nothing
else. My early days at NASA involved nothing more complex than a Friden
electric calculator and a slide rule. Few people who remember the early
NASA projects Mercury, Gemini, and Apollo realize the extent to
which it was all done without computers. True, we never could have guided
the Eagle to the surface of the Moon without its flight computer and the
software that went into it. In fact, many of the most significant developments in real-time software and digital guidance and control systems
evolved out of that effort. But all the early work was done with slide rules,
calculators, and hand-drawn graphs.
Over the years, I developed a collection of useful, general-purpose tools,
which I carried around in a wooden card case, just as a pool shark carries
his favorite cue stick. The box followed me from job to job, and I considered the contents the tools of my trade, just as real as those of a carpenter,
mechanic, or plumber.
During my years in the space program, most of the work involved
math-intensive computations. We were doing things like simulating the
motion of spacecraft or their attitude control systems. We were learning as
we went along, and making up algorithms along the way because we were
going down roads nobody had trod before. Many of the numeric algorithms
originally came from those developed by astronomers for hand computations. But we soon found that they were not always suitable for computers.
For example, when an astronomer, computing by hand, came across special
cases, such as division by a number near zero, he could see the problem
coming and devise a work-around. Or when using numerical integration, a
person computing by hand could choose a step size appropriate to the problem. The computer couldnt handle special considerations without being
told how to recognize and deal with them. As a result, many of the algorithms came out remarkably different from their classical forms. Many new,
and sometimes surprising, techniques also came out that had no classical
counterparts. As I discovered and sometimes invented new algorithms, they
found their way into that ever-growing toolbox of programs.
I was into microprocessors early on. When Fairchild came out with their
first low-cost integrated circuits ($1.50 per flip-flop bit), I was dreaming of a
home computer. Before Intel shook the world with its first microprocessor,
xxi
the 4004, I was writing code for a less general-purpose chip, but one still
capable of becoming a computer.
In late 1974, MITS shook the world again with the advent of the $349,
8080-based Altair, the computer that really started the PC revolution, no
matter what Steve Jobs says. By that time, I had already written floating-point software for its parent, the 8008. I wanted to catch this wave in
the worst way, and I did, sort of. For a time, I managed a company that had
one of the very first computer kits after the Altair. The first assembly language programs I wrote for hire were pretty ambitious: a real-time controller for a cold forge, written for the 4040, and a real-time guidance system,
using floating-point math and a Kalman filter, to allow an 8080 to steer a
communications satellite dish. Both worked and outperformed their
requirements.
In 1975, before Bill Gates and Paul Allen had written Altair (later
Microsoft) BASIC, we were building and selling real-time systems based on
microprocessors, as well as a hobby computer kit. So why aint I at least a
millionaire, if not a billionaire? Answer: some luck, and a lot of hard work.
You have to work hard, with great concentration, to snatch defeat from the
jaws of victory, and thats exactly what my partners and I did. I expect we
werent alone.
I am relating this ancient history to emphasize that I think I bring to the
table a unique perspective, colored by the learning process as the industry
has developed and changed. Although I dont advocate that all other programmers of embedded systems go through the torture of those early days, I
do believe that doing so has given me certain insights into and understanding of whats required to build successful, reliable, and efficient embedded
systems. Ill be sharing those insights and understandings with you as I go.
This will not be your ordinary how-to book, where the author gives canned
solutions without explanation solutions that you can either use or not,
but cannot easily bend to your needs. Rather, Ill share alternative solutions
and the trade-offs between them so that you can use my code and have the
understanding necessary to generate your own, with a thorough knowledge
of why some things work well and some dont. That knowledge comes from
the experience Ive gained in my travels.
xxii
Preface
Section I
Foundations
Section I: Foundations
1
Chapter 1
ZERO
ONE
0
1
I can see that Im going to be doing something in a counted loop, but how
many times? To find out, I must go track down START_VALUE and END_VALUE,
which may well be in files separate from the one Im working on most
likely in a C header file or, because nature is perverse, even in separate
header files. This does nothing toward helping me to understand the program, much less maintain it. In cases where the numbers are not likely to
change, the far simpler, and more transparent
for(i = 0; i <3; i++)
5
or the ones that are likely to change in the future, like the size of data buffers. For obvious values like the dimensions of vectors in a 3-D universe,
keep it simple and use the literal.
In any case, having established that at least some literal constants deserve
to be named, the problem still remains how to best assign these names to
them and, far more importantly, how to communicate their values to the
places where they are needed. It may surprise you to learn that these questions have plagued programmers since the earliest days of FORTRAN. Back
when all programs were monstrous, monolithic, in a single file, and in
assembly language, there was no problem; in assembly, all labels are global,
so duplicate names never occurred, and the value of the constant was universally accessible from anywhere in the program.
1.0
#define Two
2.0
#define Three
3.0
It may seem frivolous to define a constant called One, and I suppose it is.
The idea does have some merit, though. A common programming error
made by beginners (and sometimes us old-timers, too) is to leave the decimal point out of a literal constant, resulting in statements such as
x = x + 1;
where x is a floating-point number. By doing this, youre inviting the compiler to generate a wasteful run-time conversion of the 1 from integer to
float. Many compilers optimize such foolishness out, but some do not. Ive
found that most modern compilers let me use integer-like constants, like 0
and 1, and are smart enough to convert them to the right type. However, to
take the chance is bad programming practice, so perhaps theres some value
to setting up 0 and 1 as floating-point constants, just to avoid the mistake.
Sadly, some FORTRAN programmers continue to use monstrous, monolithic, single-file programs, which is hardly surprising when one considers
Listing 1.1
Related Constants
You might have noticed that the constants in the example are all related to
each other and, ultimately, to . One advantage of a FORTRAN initialization section or subroutine is that the assignment statements were truly
assignment statements. That is, they represented executable code that could
Time Marches On
Listing 1.2
This code not only makes it quite clear what the variables are, but more
importantly, it leaves me with only one constant to actually type, decreasing
the risk of making an error. If I went to a machine with different precision,
changing just the one number got me back in business. The only remaining
problems with this approach were simply the bother of duplicating the COMMON statement in every subroutine that used the constants and remembering
to call the initialization subroutine early on.
Recognizing the need to initialize variables, the designers of FORTRAN
IV introduced the concept of the Block Data module. This was the one module in which assignment to COMMON variables was allowed, and it was guaranteed to set the values of all of them before any of the program was
executed. This relieved the programmer of the need to call an initializer.
FORTRAN IV also provided named common blocks, so I could create a single block named CONST with only a handful of constant values. (Earlier versions had just a single COMMON area, so the statement had to include every
single global variable in the program.)
In one sense, however, the Block Data module represented a significant
step backward. This was because the assignment statements in Block
Data only looked like assignments. They werent, really, and did not represent executable code. Therefore, the code in Listing 1.2 would not work in a
FORTRAN IV Block Data module.
Time Marches On
Except for hard-core scientific programmers, FORTRAN is a dead language. However, the need to use named constants, which involves not only
assigning them values but also computing certain constants based upon the
values of others and making them available to the modules that need them,
lives on. You might think that the designers of modern languages like C and
C++ would have recognized this need and provided ample support for it,
You can put these definitions at the top of each C file that needs them, but
that leaves you not much better off than before. Unless the project has only
one such file, you must duplicate the lines in each file of your project. This
still leaves you with multiple definitions of the constants; youve only moved
them from the middle of each file to the top.
The obvious next step is to put all the definitions in a central place
namely, a header file, as in Listing 1.3. Note that Ive put an #ifndef guard
around the definitions. It is always good practice in C to avoid multiple definitions if one header file includes another. Always use this mechanism in
your header files.
Note that Ive reverted to separate literals for each constant. This is
because the #define statement, like the FORTRAN Block Data assignments, doesnt really generate code. As Im sure you know, the preprocessor
merely substitutes the character strings inline before the compiler sees the
code. If you put executable code into the #defines, you run the risk of generating new executable code everywhere the constant is referenced.
Listing 1.3
PI
HALFPI
TWOPI
RADIANS_PER_DEGREE
DEGREES_PER_RADIAN
ROOT_2
SIN_45
3.141592654
1.570796327
6.283185307
1.74532952e-2
57.29577951
1.414213562
0.707106781
For constants such as TWOPI, this is not such a big deal. If you write
#define HALFPI
(PI/2.0)
the expression will indeed be substituted into the code wherever HALFPI is
used (note the use of the parentheses, which are essential when using the
preprocessor in this way). However, most compilers are smart enough to
recognize common, constant subexpressions, even with all optimizations
turned off, so they will compute these constants at compile time, not run
time, and execution speed will not be affected. Check the output of your
own compiler before using this trick too heavily.
But consider the constant ROOT_2. Id like to write the following statement.
#define ROOT_2 (sqrt(2.0))
However, this contains a call to a library subroutine. It is guaranteed to generate run-time code. Whats more, youd better include <math.h> in every
file in which ROOT_2 is referenced, or youre going to get a compilation error.
10
Listing 1.4
Listing 1.4
11
:=
:=
:=
:=
:=
:=
Sqrt(Two);
Sqrt(Three);
Sqrt(Five);
(Root_Five + One)/Two;
One/Root_Two;
Root_Three/Two;
You could use the Unit Constants, for example, to create functions that
convert degrees to radians. I cant remember how many lines of FORTRAN
code Ive seen devoted to the simple task of converting angles from degrees
to radians and back.
PSI
= PSID/57.295780
THETA = THETAD/57.295780
PHI
= PHID/57.295708
Not only does it look messy, but you only need one typo (as I deliberately
committed above) to mess things up.
The Unit Constants in Listing 1.4 allows you to create the following
functions.
{ Convert angle from radians to degrees }
Function Degrees(A: Real): Real;
Begin
Degrees := Degrees_Per_Radian * A;
End;
{ Convert angle from degrees to radians }
Function Radians(A: Real): Real;
Begin
Radians := Radians_Per_Degree * A;
End;
12
A C++ Solution
Can I do anything with C++ objects to get the equivalent of the package and
unit initializations? Yes and no. I can certainly define a class called Constants with one and only one instance of the class.
class Constants{
public
double pi;
double twopi;
Constants( );
~Constants( );
} constant;
I can then put the code any code to initialize these variables into
the class constructor. This solution is workable, but I am back to having to
qualify each constant as I use it, just as in Ada: constant.pi. The syntax
gets a bit awkward. Also, there must surely be some run-time overhead
associated with having the constants inside a class object.
Fortunately, if youre using C++, you dont need to bother with either
classes or preprocessor #defines. The reason is that, unlike C, C++ allows
for executable code inside initializers. Listing 1.5 is my solution for making
constants available in C++ programs. Notice that its an executable file, a
A C++ Solution
13
.cpp file, not a header file. However, it has no executable code (except
whats buried inside the initializers).
Listing 1.5
Dont forget that you must make the constants visible to the code that references it by including a header file (Listing 1.6).
Listing 1.6
double
double
double
double
double
double
double
pi;
halfpi;
twopi;
radians_per_degree;
degrees_per_radian;
root_2;
sin_45;
14
Listing 1.7
Listing 1.7
15
double
double
double
double
double
double
double
const
const
const
const
const
const
const
pi
halfpi
twopi
radians_per_degree
degrees_per_radian
root_2
sin_45
=
=
=
=
=
=
=
3.141592654;
pi / 2.0;
2.0 * pi;
pi/180.0;
180.0/pi;
sqrt(2.0);
root_2 / 2.0;
#endif
Think a moment about the implications of using the header file shown
above. Remember, this file will be included in every source file that references it. This means that the variables pi, halfpi, and so on will be defined
as different, local variables in each and every file. In other words, each
source file will have its own local copy of each constant. Thats the reason
for the keyword static: to keep multiple copies of the constants from driving the linker crazy.
It may seem at first that having different local copies of the constants is a
terrible idea. The whole point is to avoid multiple definitions that might be
different. But in this case, they cant be different because theyre all generated by the same header file. Youre wasting the storage space of those multiple copies, which in this example amounts to 56 bytes per reference. If this
bothers you, dont use the method, but you must admit, thats a small
amount of memory.
The sole advantage of putting the constants in the header file is to avoid
having to link constant.obj into every program, and by inference, to
include it in the project make file. In essence, youre trading off memory
space against convenience. I guess in the end the choice all depends on how
many programs you build using the file and how many separate source files
are in each. For my purposes, Im content to put the constants in the .cpp
file instead of the header file, and thats the way its used in the library
accompanying this book. Feel free, however, to use the header file approach
if you prefer.
16
= 4.0 * atan(1.0);
Clearly, this will work regardless of the precision you choose, the word
length of the processor, or anything else. It also has the great advantage that
you are absolutely guaranteed not to have problems with discontinuities as
you cross quadrant boundaries. You may think it is wasteful to use an arctangent function to define a constant, but remember, this is only done once
during initialization of the program.
The code shown in this chapter is included in Appendix A, as is all other
general-purpose code. Bear in mind that I have shown two versions of the
file constant.h. You should use only one of them, either the one in
Listing 1.6 or Listing 1.7.
2
Chapter 2
17
18
19
ing using macros). The fundamental math functions min() and max() are
defined as macros in the header file stdlib.h.
#define max(a,b)
#define min(a,b)
Because of this definition, I can use these functions for any numeric type.
Ill be using these two functions heavily.
I must tell you that Im no fan of the C preprocessor. In fact, Im President, Founder, and sole member of the Society to Eliminate Preprocessor
Commands in C (SEPCC). I suppose the idea that the preprocessor could
generate pseudofunctions seemed like a good one at the time. In fact, as in
the case of min()/max(), there are times when this type blindness can be
downright advantageous sort of a poor mans function overloading.
For those not intimately familiar with C++, one of its nicer features is that it
allows you to use the same operators for different types of arguments, including user-defined types. Youre used to seeing this done automatically for
built-in types. The code fragment
x = y * z;
works as expected, whether x, y, and z are integers or floating-point numbers. C++ allows you to extend this same capability to other types. However,
as noted in the text, this only works if the compiler can figure out which
operator you intended to use, and that can only be done if the arguments are
of different types.
Operator overloading is perhaps my favorite feature of C++. However, it
is also one of the main reasons C++ programs can have significant hidden
inefficiencies.
As noted, you lose any hope of having the compiler catch type conversion errors. However, the alternatives can be even more horrid, as the next
function illustrates.
The simple function abs(), seemingly similar to min() and max() in complexity, functionality, and usefulness, is not a macro but a true function.
This means that it is type sensitive. To deal with the different types, the C
founding fathers provided at least three, if not four, flavors of the function:
abs(), fabs(), labs(), and fabsl(). (This latter name, with qualifying
characters both front and rear, earns my award as the ugliest function name
20
It could be used for all numeric types, but for some reason, the founding
fathers elected not to do so. Could this be because zero really has an implied
type? Should I use 0.0L, for long double? Inquiring minds want to know. In
any case, Ive found that most modern compilers let me use integer-like constants, like 0 and 1, and are smart enough to convert them to the right type.
Perhaps the presence of a literal constant in abs() is the reason it was
defined as a function instead of a macro. If so, the reasons are lost in the
mists of time and are no longer applicable.
Whats in a Name?
Although I detest preprocessor commands, Im also no fan of the idea of
multiple function names to perform the same function. As it happens, Im
also President, Founder, and sole member of the Society to Eliminate Function Name Prefixes and Suffixes (SEFNPS). The whole idea seems to me a
coding mistake waiting to happen.
In a previous life writing FORTRAN programs, I learned that the type of
a variable, and the return type of a function, was determined via a naming
convention. Integer variables and functions had names beginning with characters I through N. All others were assumed to have floating-point values.
As a matter of fact, even before that, the very first version of FORTRAN
(before FORTRAN II, there was no need to call it FORTRAN I) was so
primitive that it required every function name to begin with an F; otherwise,
it couldnt distinguish between a function call and a dimensioned variable. I
suppose that this FORTRAN would have had FIABS(), FFABS(), and
FFABSL().
When double-precision floats came along in FORTRAN, we had the
same problem as we have now with C. While we could declare variables
either real or double precision, we had to use unique names to distinguish the
functions and subroutines. Although you could declare variables either real
or double precision, you had to use unique names to distinguish the functions
and subroutines. I cant tell you how many times Ive written math libraries
for FORTRAN programs, only to have to create another copy with names
Whats in a Name?
21
ending in D, or some similar mechanism, when the program migrated to double precision. I didnt like the idea then, and I dont like it still.
In C++ you can overload function names a feature I consider one of
the great advantages of the language despite the fact that the compiler
sometimes has trouble figuring out which function to call. This means, in
particular, that a function such as abs() can have one name and work for all
numeric types.
Unfortunately, this is not done in the standard C++ library. To assist in
portability, C++ compilers use the same math libraries as C. You can always
overload the function yourself, either by defining a new, overloaded function or by using a template (assuming you can find a compiler whose implementation of templates is stable).
Alternatively, you can always define your own macro for abs(), as in the
example above. This approach works for both C and C++. As with other
macros, you lose compile-time type checking, but you gain in simplicity and
in portability from platform to platform. Or so I thought.
Recently, I was developing a C math library that included a routine to
minimize the value of a function. Because of my innate distaste for multiple
names and the potential for error that lurks there, I included in my library
the macro definition for abs(). I thought I was making the process simpler
and more rational. Unfortunately, things didnt work out that way. The function minimizer I was building, using Microsoft Visual C/C++, was failing to
converge. It took a session with CodeView to reveal that the compiler was,
for some reason, calling the integer library function for abs() despite my
overloaded macro. The reason has never been fully explained; when I
rebooted Windows 95 and tried again the next day, the darned thing worked
as it should have in the first place. Chalk that one up to Microsoft strangeness. But the whole experience left me wondering what other pitfalls lurked
in those library definitions.
Digging into things a bit more led me to the peculiar broken symmetry
between min()/max() and abs(). The macros min and max are defined in
<stdlib.h>. The abs() function, however, is defined in both <stdlib.h>
and <math.h>. (To my knowledge, this is the only function defined in both
header files. What happens if you include both of them? I think the first one
prevails, because both files have #ifdef guards around the definitions.)
My confusion was compounded when I tried the same code on Borlands
compiler and found that the two compilers behaved differently. If you use
min() or max(), and fail to include <stdlib.h>, both the Microsoft and Borland compilers flag the error properly and refuse to link. However, if you try
the same thing with abs(), both compilers will link, and each produces a
22
to turn off the existing definition. Ironically, the Borland compiler doesnt
really seem to care if I do this or not. If I include the macro, its used properly whether I use the #undef or not. On the other hand, the #undef does
seem to help the Microsoft compiler, which appears to use the macro reliably; without it, it seems to do so only on odd days of the month or when
the moon is full. Go figure.
If theres a lesson to be learned from this, I think its simply that you
should not take anything for granted, even such seemingly simple and familiar functions as min(), max(), and abs(). If you have the slightest doubt,
read the header files and be sure you know what function youre using.
Also, check the compilers output. Dont trust it to do the right thing. In any
case, the tools in this book use the macro abs. Youd better use the #undef
too, just to be safe and sure.
Note the syntax: the value returned has the magnitude of x and the sign of y.
Sometimes, you simply want to return a constant, such as 1, with the sign
23
set according to the sign of some other variable. No problem; just call
sign() with x = 1.
(1,
(7,
(5,
(4,
4)
2)
3)
2)
=
=
=
=
1
1
2
0
and so on. In other words, the result of mod(x, y) must always be between 0
and y.
However, if you look in the appropriate language manuals, youll see
both mod() and % are defined by the following code.
int n = (int)(x/y);
x -= y * n;
Unfortunately, this code doesnt implement the rules wed like to have. Consider, for example, the following expression.
-5 % 2
24
Listing 2.1
3
Chapter 3
25
26
followed shortly by
Alarm 1202
Roughly translated, this means, Ive just gone out to lunch. Youre on
your own, kid. Having all the right stuff, Armstrong stoically took over
and landed the LEM manually. The astronauts never voiced their reactions,
but I know how Id feel on learning that my transportation seemed to be
failing, with the nearest service station 240,000 miles away. Its probably
very much the same reaction that youd get from that chemical plant manager. If Id been them, the first person Id want to visit, once I got home,
would be the programmer who wrote that code.
The need for error checking in real-time embedded systems is very different than that for batch programming. The error mechanisms in most library
functions have a legacy to the days of off-line batch programs, in which the
system manager mentality prevailed; that is, The programmer screwed up.
Kick him off the machine and make room for the next one.
An embedded system, on the other hand, should keep running at all
costs. That chemical plant or lunar lander must keep working, no matter
what. Its not surprising, then, that the error-handling behavior of the stan-
Appropriate Responses
27
dard library functions should often need help for embedded applications.
Any good embedded systems programmer knows better than to trust the
library functions blindly. Quite often, youll find yourself intercepting the
errors before they get to the library functions.
Appropriate Responses
If a global halt is an inappropriate response in an embedded system, what is
the appropriate one? That depends on the application. Usually, it means
flagging the error and continuing with some reasonable value for the function result. Surprisingly, it often means doing almost nothing at all.
To show you what I mean, I will begin with floating-point arithmetic.
The important thing to remember is that floating-point numbers are, by
their very nature, approximations. It is impossible to represent most numbers exactly in floating-point form. The results of floating-point computations arent always exactly as you expect. Numbers that are supposed to be
1.0 end up as 0.9999999. Numbers that are supposed to be 0.0 end up as
1.0e-13.
Now, zero times any number is still zero. Zero times zero is still zero. But
1.0e-27 times 1.0e-27 is underflow.
Almost all computers and coprocessors detect and report (often via interrupt) the underflow condition. When using PC-based FORTRAN, my programs occasionally halted on such a condition. This is not acceptable in
embedded systems programming.
So what can you do? Simple. For all practical purposes, anything too
small to represent is zero in my book. So a reasonable response is simply to
replace the number with 0 and keep right on going. No error message is
even needed.
Many compilers take care of underflows automatically. Others dont. If
yours doesnt, you need to arrange an interrupt handler to trap the error
and fix it.
Tom Ochs, a friend and outspoken critic of floating-point arithmetic,
would chide me for being so sloppy and glossing over potential errors. In
some cases, perhaps hes right, but in most, its the correct response. If this
offends your sensibilities, as it does Toms, its OK to send an error message
somewhere, but be aware that it could cause your customer to worry
unnecessarily. If you have a special case where underflow really does indicate an error, your program needs special treatment. But for a library routine thats going to be used in a general-purpose fashion, stick with the
simple solution.
28
Listing 3.1
29
On the Case
A word here about the name of the function in Listing 3.1 is appropriate.
Every function in a C program needs a unique name. C++ function names
can be overloaded, but only if their arguments have different types. In this
case, they dont, so overloading doesnt work.
C and C++ are supposed to be case sensitive. Variable names and functions are considered different if they have different combinations of upperand lowercase characters. The variables Foo, foo, FOO, and fOO are all different variables. The Microsoft Visual C++ environment, however, doesnt follow this rule. Visual C++ uses the standard Microsoft linker, which doesnt
understand or distinguish case differences.
When I started writing safe functions, I wondered what I should call
them. Names like my_sqrt() and my_atan() seemed trite. Sometimes I tacked
on a leading j (for Jack), but that solution always seemed a bit egotistical.
The other day, I thought I had the perfect solution. Id simply use the
same name as the library function, but with an uppercase first letter: Sqrt()
instead of sqrt(). This seemed like the perfect solution: familiar, easy to
remember names, but also unmistakably unique and recognizably my own.
With this approach, I not only could get more robust behavior, I could also
eliminate the hated type-qualifying characters in the function names. So,
originally, I called the function in Listing 3.1 Sqrt().
Imagine my surprise when I tried this on the Microsoft compiler and
found that it insisted on calling the library function instead. A quick session
with CodeView revealed the awful truth: Microsoft was calling _sqrt(),
which refers to the library function. It was ignoring the case in my function
name!
The problem is, I didnt realize the Microsoft linker wouldnt distinguish
case differences. Thus, even though the names sqrt() and Sqrt() are supposed to be unique according to the C rules of engagement, the Microsoft
linker misses this subtlety and sees them as the same.
I also dealt with yet another feature of the Microsoft development
environment. In every language Ive ever used since programming began, any
function thats supplied locally by the user took precedence over library functions with the same name. Not so with Visual C++. That system gives the
library function preference and ignores the user-supplied function. I learned
this the hard way when trying to overload abs().
Ive never figured a work around for this one. To avoid the problem and
any future compiler strangeness, I took the next best option and chose a
function name I was sure wouldnt confuse the linker. At the moment, Im
reduced to using unique but longer names, like square_root() and arcsine(), that the compiler has no choice but to recognize as uniquely mine.
30
x){
Is It Safe?
31
argument and call the double function, anyhow. In short, the compiler cant
be trusted to call the right function.
The third function does at least make sure that the correct function is
called, and the number is not truncated. If you have a compiler that really,
truly uses long doubles, you may want to seriously consider including overloadings for this type. However, my interest in long-double precision waned
quickly when I learned that the compilers I use for 32-bit applications do not
support long double anymore. The 16-bit compilers did, but the 32-bit ones
did not. Why? Dont ask me; ask Borland and Microsoft.
I ran into trouble with type conversions; not in the code of jmath.cpp, but
in the definitions of the constants. If Im going to use long doubles, then I
should also make the constants in constant.h long double. But whenever I
did that, I kept getting compiler errors warning me that constants were being
truncated.
In the end, long double arithmetic proved to be more trouble than it was
worth, at least to me. So the code in the remainder of this book assumes double-precision arithmetic in all cases. This allowed me to simplify things quite
a bit, albeit with a considerable loss of generality.
At this point, someone is bound to ask, Why not use a template? The
answer is Keep It Simple, Simon (KISS). Although I have been known to use
C++ templates from time to time, I hate to read them. They are about as
readable as Sanskrit. I also have never been comfortable with the idea of
sticking executable code in header files, which is where many compilers insist
that templates be put. P.J. Plauger says his subset embedded C++ wont have
templates, and thats no great loss, so Ill avoid them in this book. So much
for the Standard Template Library (STL).
Is It Safe?
I must admit that the approach used in square_root() simply ignoring
negative arguments can mask true program bugs. If the number computed really is a large negative number, it probably means that something is
seriously wrong with the program. But it doesnt seem fair to ask the library
functions to find program bugs. Instead of sprinkling error messages
through a library of general-purpose routines, wouldnt it be better to debug
the program? If the application program is truly robust and thoroughly
tested, the condition should never occur.
If it really offends your sensibilities to let such errors go unreported, you
might modify the function to allow a small epsilon area around zero and
to report an error for negative numbers outside this region. Be warned,
32
Listing 3.2
Taking Exception
33
return -halfpi;
return asin(x);
}
// Safe inverse cosine
double arccos(double x)
{
return(halfpi arcsin(x));
}
// safe, four-quadrant arctan
double arctan2(double y, double x)
{
if(y == 0)
return(x>=0)? 0: -pi;
return atan2(y, x);
}
// safe exponential function
double exponential(double x){
if(x > 46)
return BIG_FLOAT;
return exp(x);
}
Taking Exception
A lot of the headaches from error tests could be eliminated if the programming language supported exceptions. Im a big fan of exceptions, when
theyre implemented properly. They are wonderful way to deal with exceptional situations without cluttering up the mainstream code with a lot of
tests. Unfortunately, I havent found a single compiler in which exceptions
have been implemented properly. Most implementations generate very, very
inefficient code far too inefficient to be useful in a real-time program.
34
Exception Exemption
For those not familiar with the term, an exception in a programming language is something that happens outside the linear, sequential processing normally associated with software. Perhaps the form most familiar is the BASIC
ON ERROR mechanism. The general idea is that, in the normal course of
processing, some error may occur that is abnormal; it could be something
really serious, like a divide-by-zero error usually cause for terminating the
program. Or, it could be something that happens under normal processing,
but only infrequently, like a checksum error in a data transfer.
In any case, the one thing we cannot allow is for the program to crash.
The main reason compiler writers allow fatal errors to terminate programs is
that its difficult to define a general-purpose behavior that will be satisfactory
in the particular context where the error occurs.
The general idea behind exception handling is that when and where an
exception occurs, the person best able to make an informed decision as to
how to handle it is the programmer, who wrote the code and understands the
context. Therefore, languages that support exceptions allow the programmer
to write an exception handler that will do the right thing for the particular
situation. As far as I know, all such languages allow an exception to propagate back up the call tree; if the exception is not handled inside a given function, its passed back up the line to the calling function, etc. Only if it reaches
the top of the program, still unhandled, do we let the default action occur.
One point of debate is the question: what should the program do after the
exception is handled? Some languages allow processing to resume right after
the point where the exception occurred; others abort the current function.
This is an issue of great importance, and has a profound impact upon the
usability of exceptions in real-world programs.
35
Ironically, one language that does support exceptions, and supports them
efficiently at that, is BASIC, with its ON condition mechanism. I say ironically
because BASIC is hardly what one usually thinks of as a candidate for
embedded systems programming.
Another language that supports exceptions is Ada. In fact, to my knowledge Ada was the first language to make exceptions an integral part of the
language. (Trivia question: What was the second? Answer: Borlands shortlived and much-missed Turbo Modula 2, which was available on CP/M
machines only and dropped by Borland because they never got a PC version
working.)
Again, though, theres a certain irony, because even though exceptions
were designed into the language, and I know in my heart that the language
designers intended for them to be implemented efficiently (mainly because
they told me so), thats not how they actually got implemented. At least the
earlier Ada compilers suffered from the exception-as-fatal-error syndrome,
and their writers implemented exceptions so inefficiently that they were
impractical to use. In most Ada shops, in fact, the use of them was verboten.
I suspect, but cannot prove, that this mistake was made because the Ada
compiler developers were not real-time programmers, and didnt appreciate
the intent of the language designers.
Unless youve been stranded on the Moon, you probably know that the
current standard for C++ provides for exceptions, and most compilers now
support them. The exception facility in C++ seems to be in quite a state of
flux, with compiler vendors struggling to keep up with recent changes to the
standards. However, from everything Ive heard, theyre at least as inefficient in C++ as the early Ada compilers. Whats more, because of the way
exceptions are defined in C++, there is little or no hope that they will ever be
more efficient. Exceptions, as Ive already noted, are eliminated from the
upcoming embedded subset language.
Because C doesnt support exceptions, C++ implements them poorly, and
the embedded subset wont support them at all, Im afraid the great majority
of embedded systems programmers will have to make do with if tests sprinkled throughout the code, as in the listings of this chapter.
36
tan(x) = sin(x)/cos(x)
This one is often left out of libraries for the very reason that its unbounded;
if x = 90, tan(x) = . If you blindly implement Equation [3.1], youll get
a divide-by-zero error for that case. Again, the trick is to test for the condition and return a reasonable value in this case, a machine infinity
that is, a very large number. It should not be the maximum floating-point
number, for a very good reason: you might be doing further arithmetic with
it, which could cause overflow if its too big. To take care of this, Ive added
the definition
const long double BIG_FLOAT
= 1.0e20;
x
1
1
sin ( x ) = tan ------------------2-
1x
37
Again, you cant allow arguments outside the range 1.0, so the routine
tests for that case before calling the square root. Note that calling the safe
version, square_root(), described earlier in this chapter, doesnt help here:
it would just lead to a divide-by-zero exception.
Its worth pointing out that the accuracy of Equation [3.2] goes to pot
when x is near 1.0. Its tempting to try to fix that, perhaps by some transformation in certain ranges. Forget it. The problem is not with the math,
but with the geometry. The sine function has a maximum value of 1.0 at
90, which means that the function is flat near 90, which in turn means that
the inverse is poorly defined there. There is no cure; its a consequence of the
shape of the sine curve.
A listing of the inverse sine, for those whose programming language
doesnt support it, is shown in Listing 3.3. It is inherently safe, because we
test the value of x before we do anything. Remember, you dont need this
function if your programming language already supports it. You only need
the safe version of Listing 3.2.
An equation similar to Equation [3.2] is also available for the inverse
cosine.
[3.3]
1
2
1
1 x - .
cos ( x ) = tan ----------------- x
However, Equation [3.3] is not the one to use. First of all, it returns a value
in the range 90, which is wrong for the arccosine. Its principal value
should be in the range 0 to 180. Worse yet, the accuracy of the equation
goes sour at x = 0.0, which is where it really should be the most accurate. A
far better approach is also a simpler one: use the trig identity
cos ( x ) = sin ( 90 x ) ,
which leads to
1
cos ( x ) = 90 sin ( x ) ,
38
the value of the sine is not enough to determine which quadrant the result
should be in. Accordingly, both atan() and asin() return results only in
quadrants one and four (90 to +90), whereas acos() returns results in
quadrants one and two (0 to 180). Typically, programmers have to use
other information [like the sign of the cosine if asin() is called] to decide
externally if the returned value should be adjusted.
The four-quadrant arctangent function solves all these problems by taking two arguments, which represent or are at least proportional to, the
cosine and the sine of the desired angle (in that order). From the two arguments, it can figure out in which quadrant the result should be placed with
no further help from the programmer.
Why is the four-quadrant arctangent so valuable? Simply because there
are so many problems in which the resulting angle can come out in any of
the four quadrants. Often, in such cases, the math associated with the problem makes it easier to compute the sine and the cosine separately, rather
than the tangent. Or, more precisely, its easy to compute values which are
proportional to these functions, but not necessarily normalized. We could
then use an arcsine, say, and place the angle in the right quadrant by examining the sign of the cosine (what a tongue-twister!). But the four-quadrant
arctangent function does it so much more cleanly.
This function is a staple of FORTRAN programs, where its known as
atan2(). The name doesnt hold much meaning, except to FORTRAN programmers [youd think theyd at least call it ATAN4()], but its so well known
by that name that it also appears by the same name in the C library.
The atan2() function takes two arguments and is defined as
atan2(s, c) = tan-1(s/c)
with the understanding that the signs of both s and c are examined, not just
the sign of the ratio, to make sure the angle comes out in the correct quadrant. (Actually, both atan2 and atan do a lot more than that. To avoid inaccuracies in the result when the argument is small, they use different
algorithms in different situations.) Note carefully the order: the s (sine function) value then c. Note also that it is not necessary to normalize s and c to
the range 1 to 1; as long as they are proportional, the function will take
care of the rest.
39
Listing 3.3
40
Section II
Fundamental Functions
41
42
4
Chapter 4
Square Root
A fundamental function is like an old, comfortable pair of slippers. You
never pay much attention to it until it turns up missing. If youre doing some
calculations and want a square root or sine function, you simply press the
appropriate key on your calculator, and theres the answer. If you write programs in a high-order language like C, C++, Pascal, or Ada, you write
sqrt(x) or sin(x), and thats the end of it. You get your answer, and you
probably dont much care how you got it. I mean, even a three dollar calculator has a square root key, right? So how hard could it be?
Not hard at all, unless, like your slippers, you suddenly find yourself in a
situation where the fundamental functions arent available. Perhaps youre
writing in assembly language for a small microcontroller or youre programming using integer arithmetic to gain speed. Suddenly, the problem begins to
loom larger. Because the fundamental functions are well fundamental,
they tend to get used quite a few times in a given program. Get them wrong,
or write them inefficiently, and performance is going to suffer.
Most people dont make it their lifes work to generate fundamental
functions we leave that to the writers of run-time libraries so its difficult to know where to turn for guidance when you need it. Most engineering or mathematical handbooks will describe the basic algorithms, but
sometimes the path from algorithm to finished code can be a hazardous one.
43
44
1 a
x = --- --- + x
2 x
where a is the input argument for which you want the square root, and x is
the desired result. The equation is to be applied repetitively, beginning with
some initial guess, x0, for the root. At each step, you use this value to calculate an improved estimate of x. At some point, you decide the solution is
good enough, and the process terminates.
Note that the formula involves the average of two numbers: a guess, x,
and the value that you get by dividing x into the argument. (Hint: dont
start with a guess of zero!) If you have trouble remembering the formula,
never fear; Im going to show you how to derive it. You can take this lesson,
one of the best lessons I learned from my training in physics, to the bank:
Its better to know how to derive the formula you need than to try to
memorize it.
45
To derive Newtons method for square root, I will begin with an initial
estimate, x0. I know that this is not the true root, but I also know, or at least
hope, that its close. I can write the true root as the sum of the initial estimate plus an error term.
[4.2]
x = x0 + d
This is, youll recall, the true root, so its square should equal our input, a.
a = x
or
[4.3]
a = x 0 + 2x 0 d + d .
Dont forget that the initial estimate is supposed to be close to the final root.
This means that the error d is hopefully small. If this is true, its square is
smaller yet and can be safely ignored. Equation [4.3] then becomes
2
a x 0 + 2x 0 d ,
[4.4]
a x0
d -------------2x 0
Now that I have d, I can plug it into Equation [4.4] to get an estimate of x.
2
ax
x x 0 + -------------02x 0
Simplifying gives
2
2x 0 + a x 0
x ----------------------------2x 0
or
2
[4.5]
x0 + a
x -------------- .
2x 0
46
2 x0
Table 4.1
Guess
Square
New
Guess
Error
1
2
3
4
4
2.250000000
1.569444444
1.421890363
16
5.062500000
2.463155863
2.021772207
2.250000000
1.569444444
1.421890364
1.414234286
0.835786438
0.155230882
0.007676802
0.000020724
47
Trial
Guess
Square
New
Guess
5
6
7
8
9
1.414234285
1.414213563
1.414213562
1.414213563
1.414213562
2.000058616
2.000000002
1.999999999
2.000000002
1.999999999
1.414213563
1.414213562
1.414213563
1.414213562
etc.
Error
0.000000001
0.000000000
0.000000001
0.000000000
This first example has some valuable lessons. First, you can see that convergence is rapid, even with a bad initial guess. In the vicinity of the root,
convergence is quadratic, which means that you double the number of correct bits at each iteration (note how the number of zeros in the error doubles
in steps 3 through 6). That seems to imply that even for a 32-bit result, you
should need a maximum of six iterations to converge. Not bad. Also, note
that you sneak up on the root from above the guess is always a little
larger than the actual root, at least until you get to step 7. Ill leave it to you
to prove that when starting with a small initial guess the first iteration is
high and youd still approach the root asymptotically from above.
Perhaps most important, at least in this example, is that the process
never converges. As you can see, an oscillation starts at step 7 because of
round-off error in my calculator. When using Borland C++, it doesnt occur.
Ive often seen the advice to use Equation [4.1] and iterate until the root
stops changing. As you can see, this is bad advice. It doesnt always work.
Try it with your system and use it if it works. But be aware that its a dangerous practice.
I said that the convergence is quadratic as long as youre in the vicinity of
the root. But what, exactly, does that mean, and what are the implications?
To see that, Ill take a truly awful first guess (Table 4.2).
Table 4.2
Guess
1
2
3
4
5
1.000000 e30
5.000000 e29
2.500000 e29
1.250000 e29
6.250000 e28
48
Trial
Guess
6
7
8
3.125000 e28
1.562500 e28
etc.
I wont belabor the point by completing this table for all of 105 steps it
takes to converge on the answer. I think you can see whats happening. Far
from doubling the number of good bits at each step, all youre doing is halving the guess each time. Thats exactly what you should expect, given Equation [4.5]. Because the value of a is completely negligible compared to the
square of x, the formula in this case reduces to
[4.6]
x
x ----0- .
2
At this rate you can expect to use roughly three iterations just to reduce the
exponent by one decimal digit. Its only when the initial guess is close to the
root that you see the quadratic convergence. A method that takes 105 iterations is not going to be very welcome in a real-time system.
You may think that this is an artificial example; that its dumb to start
with such a large initial guess. But what if you start with 1.0 and the input
value is 1.0e30? From Equation [4.5], the next guess will be 5.0e29, and
youre back in the soup.
It is amazing that such a seemingly innocent formula can lead to so many
problems. From the examples thus far, you can see that
you need a criterion for knowing when to stop the iteration; testing for
two equal guesses is not guaranteed to work,
to get the fast, quadratic convergence, you need a good initial guess, and
the intermediate values (after the first step) are always higher than the
actual root.
In the remainder of this chapter, Ill address these three points.
49
[4.7]
d
RE = --- ,
x
and quit when its less than some value. Thats the safest way, but its also
rather extravagant in CPU time, and that can be an issue. In fact, the fastest
and simplest, way of deciding when to stop is often simply to take a fixed
number of iterations. This eliminates all the tests, and its often faster to perform a few extra multiplies than to do a lot of testing and branching. A
method that iterates a fixed number of times is also more acceptable in a
real-time system.
The best way to limit the number of iterations is simply by starting with
a good initial guess.
50
51
hex, 40 to 3f. To keep the exponent positive, I add 0x40 to all exponents,
so they now cover the range 0 to 7f. This concept is called the split on nn
convention, and as far as I know, almost every industrial-strength format
uses it.
A picture is worth a thousand words. Lets see how the method works
with a hypothetical floating-point format. Assume seven bits of exponent,
sixteen bits of mantissa, and no phantom bit. Ill write the number in the
order: sign, exponent, mantissa (which is the usual order). Ill split the exponent on 0x40, or 64 decimal. Numbers in the format might appear as in
Table 4.3.
Table 4.3
Floating-point representation.
Value
Sign
Exponent
Mantissa
Number in
Hex
1.0
41
8000
41.8000
2.0
42
8000
42.8000
3.0
42
c000
42.c000
4.0
43
8000
43.8000
5.0
43
a000
43.a000
8.0
44
8000
44.8000
10.0
44
a000
44.a000
16.0
45
8000
45.8000
0.5
40
8000
40.8000
0.25
3f
8000
3f.8000
0.333333333
3f
5555
3f.5555
42
c90f
42.c90f
2.71e20
00
8000
00.8000
9.22e18
7f
ffff
7f.ffff
1.0
41
8000
c1.8000
Note that the integers less than 65,536 can be represented exactly
because the mantissa is big enough to hold their left-shifted value. Note also
that the decimal point is assumed to be just left of the mantissa; Ive shown
it in the hex number column for emphasis. Finally, note that the high bit of
52
Table 4.4
Exponent
Bits
Mantissa
Bits
Phantom
Bit?
Total
Bits
float
sometimes (!)
32
double
10
2425 a
54
yes
64
long double
15
64
no
80
a. The float format uses an exponent as a power of four, not two, so the high bit may not
always be 1.
53
Based on the size of the two fields, I can estimate both the dynamic range
and precision of the number. Rough estimates are shown in Table 4.5.
Table 4.5
Range
Accuracy
(decimal digits)
float
1038
67
double
10308
16
long double
104,032
19
The dynamic range of these numbers, even the simple float format, is
hard to get your arms around. To put it into perspective, there are only
about 1040 atomic particles in the entire Earth, which would almost fit into
even the ordinary float format. One can only speculate what situation
would need the range of a long double, but in the real world, you can be
pretty sure that you need never worry about exponent overflow if you use
this format.
The less said about the float format the better. It uses a time-dishonored
technique first made popular in the less-than-perfect IBM 360 series and
stores the exponent as a power of something other than two (the Intel format uses powers of four). This allows more dynamic range, but at the cost
of a sometimes-there, sometimes-not bit of accuracy.
Think about it. If you have to normalize the mantissa, you must adjust
the exponent as well by adding or subtracting 1 to keep the number the
same. Conversely, because you can only add integral values to the exponent,
you must shift the mantissa by the same factor. This means that the IBM
format (which used powers of 16 as the exponent) required shifts of four
bits at a time. Similarly, the Intel format requires shifts by two bits. The end
result: you may have as many as one (Intel) or three (IBM) 0 bits at the top
of the word. This means you can only count on the accuracy of the worst
case situation. In the case of floats on an Intel processor, dont count on
much more than six good digits.
One other point: Intel uses a split on the exponent thats two bits smaller
than usual, to make the range for large and small numbers more symmetrical. They also use the phantom bit. Thus, in their double format, 1.0 is
stored as
3ff.(1)00000 00000000.
54
Listing 4.1
hack_structure x;
void main(void){
short expo;
unsigned short sign;
while(1){
// get an fp number
cin >> x.fp;
// grab the sign and make it positive
sign = ((long)x.hi < 0);
x.hi &= 0x7fffffff;
55
Listing 4.1
56
Listing 4.2
A little experimenting with this code should convince you that I always
get convergence in, at most, five iterations for all values of input. Whenever
the input is an even power of two, convergence is immediate. Not bad at all
for a quick and dirty hack. If you need a floating-point square root in a
hurry, here it is.
Take special note that things get a little interesting when I put the exponent and mantissa back together if the original exponent is odd. For the
even exponents, I simply divide by two. If the exponent is odd, I must multiply by the factor 2 to allow for half a shift. Some folks dont like this
extra complication. They prefer to use only even exponents. You can do
this, but you pay a price in a wider range for the mantissa, which can then
vary by a factor of four, from 0.5 to 2.0. This leads to one or two extra steps
of iteration. Personally, I prefer the method shown because it gives me fewer
iterations, but feel free to experiment for yourself.
57
Figure 4.1
Square root.
It turns out that the optimal first guess is the geometric mean of the two
extreme values, because this value gives a relative error thats the same at
both ends of the range. For this range, the value is
[4.8]
1
x 0 = ----------- = 0.840896.
2
Applying the iterator of Listing 4.2 with this starting value and an input of
1/ (one extreme of the input range) gives the sequence shown in Table 4.6,
2
which reaches convergence in four iterations in the worst case.
58
Table 4.6
Guess
Error
1
2
3
4
7.1774998634e01
7.0718569294e01
7.0710678559e01
7.0710678119e01
1.0643205154e02
7.8911751189e05
4.4028638513e09
0.0000000000e+00
Can I improve on this? Again, the answer is yes. Ive seen a number of
ways to do this. The most obvious is to use a table of starting values based
on the value of the mantissa. Using a full table lookup would take up
entirely too much space, but you can use the leading few bits of the mantissa
to define a short table lookup. I wont go into detail on the method here,
because I think I have a much better approach. However, the general idea of
the table lookup approach is shown in the code fragment below.
switch((x.hi >> 14) & 3){
case 0:
r = 0.747674391;
break;
case 1:
r = 0.827437730;
break;
case 2:
r = 0.900051436;
break;
case 3:
r = 0.967168210;
break;
}
This code splits the range into four segments based on the first two bits in
the range, then it uses the optimum starting value for that range.
59
[4.9]
( x) =
x.
Using a constant value of 0.8409 is equivalent to assuming a straight horizontal line. Thats a pretty crude approximation, after all. Surely I can do
better than that.
Using a table of values, as suggested above, is equivalent to approximating the function by the staircase function shown in Figure 4.2. But even better, why not just draw a straight line that approximates the function? As
you can see, the actual curve is fairly straight over the region of interest, so a
straight line approximation is really a good fit.
Figure 4.2
Initial guesses.
r ( x ) = A + Bx .
All I have to do is find the best values for the constants A and B. The derivation is much too tedious to give in all its glory here, but the concept is simple enough, so Ill outline it for you.
First, note that you dont want the straight line to actually touch the true
function at the end points. That would make the error one-sided. Instead,
try to locate the points so that theyre higher than the curve at those
60
( x) r ( x)
E ( x ) = -------------------------( x)
and insist that E(0.5) = E(1.0). Doing this gives a relation between A and B
that turns out to be
[4.12]
B =
2A .
r ( x ) = A ( 1 + 2x ) .
To find A, note that there is some point P2 where the curve rises above the
straight line to a maximum. I dont know where this point is, but a little calculus reveals that the error is maximum at 1/2 = 0.70711. This number is
my old friend, the geometric mean. Forcing the error at this point to be the
same (but in the opposite direction) as at the extremes, I finally arrive at the
optimal value
[4.14]
1
A = ---------------------------------- .
1 1
2 + --- + ------2
2
Numerically,
A = 0.41731924
and
[4.15]
B = 0.59017853.
r ( x ) = 0.41731924 + 0.59027853x
matches the sqrt() curve nicely with a relative error less than 1.0075 at the
three extreme points. With Equation [4.16], the initial guess will never be
off by more than 0.8 percent. Using this approach, I get convergence as
shown in Table 4.7.
61
Table 4.7
Guess (r)
Error
1
2
3
7.0712650885e01
7.0710678146e01
7.0710678119e01
1.9727663130e05
2.7557689464e10
0.0000000000e+00
This is a remarkable result, because by using this optimal guess, I get convergence in a maximum of three iterations. For 32-bit floating-point accuracy (equivalent to C float, roughly six digits), convergence is good enough
in only one iteration. Thats a result pretty hard to improve upon.
Its worth mentioning that any general-purpose square root function
should be good to full machine accuracy, but there are times when speed is
more important than accuracy (otherwise, you could simply use the C function). Its important to keep the accuracy issue in focus. The long string of
zeros above is nice to see, but not always necessary. For a proper perspective, look at the accuracy expressed as a percent error in Table 4.8.
Table 4.8
Percent Error
0
1
2
0.75
0.003
0.00000004
62
Putting it Together
All that remains is to put the concepts together into a callable function, with
proper performance over the exponent range. This is shown in Listing 4.3.
Note that Ive added the usual error check for a negative argument. I also
have to check for zero as a special case. Finally, because I only need, at
most, three iterations, Ive taken out the testing loop and simply executed
three steps inline.
Listing 4.3
Putting it together.
// Model for fast square root with optimal first guess
double my_sqrt(double a){
short expo;
double factor = root_2/2;
const double A = 0.417319242;
const double B = 0.590178532;
hack_structure x;
double root;
// check for negative or zero
if(a <= 0)
return 0;
x.fp = a;
// grab the exponent
expo = (short)(x.n.hi >> 20);
expo -= 0x3fe;
// normalize the number
x.n.hi &= 0x000fffff;
x.n.hi += 0x3fe00000;
// get square root of normalized number
// generate first guess
root = A + B * x.fp;
// iterate three times (probably overkill)
Listing 4.3
63
I think you will find that Listing 4.3 handles all input values with accuracy and speed. This routine can serve as a template for a very fast assembly
language function. If you need more speed and can tolerate less accuracy,
leave out one, two, or even all three of the iterations.
64
This is an iterative algorithm, and it will result in a variable number of iterations. Note that Ive used the error, as defined by Equation [4.4], as a stop
criterion, but that it does not really enter into the computation for x. This
seems like a case of redundant code, and so it is. In fact, to get a reliable
65
stop criterion, Ive just about doubled the amount of work the CPU must do
per iteration. Whats more, there are many input values for which the algorithm takes an unnecessary extra iteration step.
Id like to be able to tighten this algorithm up a bit, but every attempt
that Ive made to do so has failed. Youre welcome to try, and I would welcome any improvement thats suggested, but my advice is simple: this algorithm is fragile; DONT MESS WITH IT!
Another problem with the integer algorithm is that it has to cover an
incredible range of numbers. In the floating-point case, I could limit the
attention to the highlighted area of Figure 4.1, and optimize the initial guess
for that area. For integers, though, the range is much broader, which generally leads to more iterations before convergence.
A
B = ----------- .
mn
That result looks pretty messy, but its not quite as bad as it seems. Remember that the limits m and n are often powers of two in fact, even powers
of two. For example, m is often unity. For such cases, the formulas for the
range of a 16-bit unsigned integer simplify to
512
A = --------289
and
[4.18]
A
B = --------- .
256
66
Table 4.9
255
32,767
65,535
1.27949892
1.7328374
1.7716255
1.98154732
0.08012533
0.00957281074
0.00692046492
4.2760179e5
231 1
20.47996875
30.84313598
0.00500003052
4.15981114e5
231 1
443.60684344
3.73932609e5
232 1
27,348.6708
9.00515849e6
231 1
65,535
Table 4.10
67
Rational coefficients.
m
n
1
1
1
256
65,536
255
32,767
231 1
65,535
231 1
B
1
2
2
1/12
1/104
1/23,386
21
444
1/200
1/26,743
These numbers dont look bad at all. Figure 4.3 shows an example of the fit.
Its not as elegant as the floating-point case, but its not bad. Because the
integer case must cover such a wide range, the curve of the square root function is not nearly as linear as for the floating-point case, and you can expect
the straight line fit to be considerably less accurate. Even so, the largest relative error is less than 40 percent, which means the function should converge
in three or four iterations.
Figure 4.3
As a final tweak, you can try to limit the range a bit by looking at the values of the numbers. For example, if the high half of a double word is 0, I
know that the integer cant be larger than 65,535, so I choose the coeffi-
68
Despite all my carping about the integer algorithm, the final results are
pretty darned good. Within the range 1 to 256, convergence always occurs
in, at most, two iterations, despite the sloppier fit. Over the entire range, the
worst case takes five iterations. I think thats about the best you can hope
for with integer arithmetic.
Good-Bye to Newton
Ive pretty much beaten poor Dr. Newton to death, and you now have two
algorithms that work well. The integer algorithm is perhaps not as satisfying as the floating-point algorithm, but it gets the job done.
Which do you use? The answer depends on the application and the CPU
hardware. If your system has a math coprocessor chip, my advice is to forget the integer algorithm and use floating-point math. Even if youre using
integer arithmetic for everything else, it might be worthwhile to convert to
floating point and back again.
On the other hand, if your CPU uses software floating point, the integer
algorithm is definitely for you.
Good-Bye to Newton
69
Ive still not exhausted the subject of square roots, because there are
other methods in addition to Newtons. Ill wrap up this chapter by taking a
look at some of these other methods.
Successive Approximation
The Newtonian methods, especially using the optimal initial guess, are
rather sophisticated as square root algorithms go. Now Ill take a trip from
the sublime to the ridiculous and ask what is the dumbest algorithm I can
cook up.
Why would I want a dumb algorithm? Because I might be using a dumb
CPU. Newtons method is fine if the CPU has built-in operations for fast
multiply and divide, but this book is about embedded systems programming, and many embedded systems still use simple eight-bit controllers that
are not blessed with such luxuries. For such processors, I may not be interested in raw speed so much as simplicity.
For the case of integer square roots, Ill be specific about what I want: I
want the largest integer x whose square is less than a. I can find such an
integer with an exhaustive search by starting with x = 1, incrementing x
until its too big, then backing off by one. Listing 4.4 shows the simplest
square root finder you will ever see. Note carefully the way the loop test is
written. The <= sign is used, rather than < to guarantee that the program
always exits the loop with x too large. Then I decrement x by one on the
way out.
Listing 4.4
The successive approximation method provides the ultimate in simplicity. Basically, its no more than trial and error. Simply try every possible integer until you find the one that fits. The code in Listing 4.4 does just that. Its
certainly neither pretty nor fast, but it sure gets the answer, and without any
convergence concerns. It also does it in four lines of executable code, which
surely must make it the smallest square root finder I can write.
70
Table 4.11
Integer squares.
n
n2
Difference
0
1
2
3
4
5
6
0
1
4
9
16
25
36
1
3
5
7
9
11
The key thing to notice is the last column, which is the difference between
successive squares. Note that this difference is simply a progression of all
odd integers. Whats more, this difference is closely related to the root. Add
one to the difference, divide by two, and you have the integer. The next
equation will make the relationship obvious:
[4.19]
( n + 1 ) = n + 2n + 1 .
Good-Bye to Newton
71
Listing 4.5
Listing 4.6
72
Listing 4.6
73
punch-cachunk, punch-punch-cachunk-DING, punch-punch-DING-clangclang in a repeated rhythm. I thought my mate had either lost his marbles,
or he was creating some new kind of computer game.
I asked him what the heck he was doing. He said, finding a square
root.
But, um , said I, this isnt a square root Friden.
I know, he said, thats why I have to do it this way.
It turns out that my office mate, Gap, was an old NASA hand and one
who could make the Friden do all kinds of tricks, including the famous
Friden March (a division of two magic numbers that made the thing
sound like the rah, rah, rah-rah-rah of a football cheer).
Somewhere along the line, Gap had learned the Friden algorithm for
finding a square root on a non-square-root Friden. It was a very similar
algorithm, in fact, to the one programmed into the cam-and-gear ROM
of the square root version. Gap taught it to me, and now Ill teach it to you.
(Gap, if youre still out there and listening, thanks for the education.)
The method involves a minor rewrite to Equation [4.19].
[4.20]
(n + 1) = n + n + (n + 1)
The keys on the Friden keyboard were sticky, meaning they held whatever was punched into them. The nice part about the formulation in Equation [4.20] is that the last digit punched in was also the number whose
square you were taking.
To see the algorithm work, watch the squares develop in Table 4.12.
Table 4.12
Numbers
Added
Result
0
1
4
9
16
25
36
0+1
1+2
2+3
3+4
4+5
5+6
6+7
1
4
9
16
25
36
49
74
Initial
Number
Numbers
Added
Result
49
64
7+8
8+9
64
81
As you can see, the number in the third column of this table is always the
square of the last number added in.
Except Gap wasnt really adding, he was subtracting from the number
whose root he was seeking. The DING sound I had heard occurred each
time the subtraction caused an overflow. This was the signal to Gap that
hed gone too far, so hed back up by adding the last two numbers back in
the reverse order hed subtracted. In that way, the number remaining in the
keyboard was always the largest digit whose square didnt exceed the input
number. This is precisely the number I seek for the integer square root.
Getting Shifty
This next part of the algorithm is critically important, both for the Friden
solution and for my computerized solution. Its the notion of shifting in a
manner similar to that used in long division.
Consider a two-digit number n, where the digits are a and b. I can write
this as
[4.21]
n = am + b ,
where m is the modulus of the number (10 in the decimal system). Then n 2
is given by
[4.22]
n = a m + 2abm + b .
You can think of this as another number with the modulus equal to m 2 and
the first digit equal to a 2. Its not immediately obvious that there are no
carries from that next term, 2abm, into the m 2 column, but that is in fact
the case. Because b must be less than m, I can write
2
2 2
n < a m + 2am + m n
or
[4.23]
n < [ ( a + 1 )m ] .
< ( a + 2a + 1 )m
75
In other words, no matter how large b is, it can never be large enough to
change the value of a. This is very important, because it means I can solve
for a independently from b.
Put another way, just as in long division, once you solve for a digit, its
solved forever. The only difference between the square root and the division
algorithms is that you have to consider the digits of the operand two at a
time (modulus m 2), instead of one at a time. In other words, if you work in
decimal arithmetic, you operate on numbers in the range 0 to 99. Thats the
reason for the old point off by twos rule you may recall from high school
(see The High School Algorithm, page 78). This is best illustrated by an
example, shown in Table 4.13. Here, Ill take the square root of the decimal
integer 1,234,567,890.
The first step is to point off, or group the digits into pairs.
12 34 56 78 90
Do this from the right starting with the decimal point. Note that this
means the leftmost group can have either one or two digits. We do this
because well be solving for a (in the formulas above) one digit at a time.
Since the square of a one-digit number can produce a two-digit result, this
requires dealing with the square two digits at a time.
Beginning at the left in Table 4.13, our first pair of digits is 12. Therefore, we begin by finding the largest integer whose square is less than 12.
This much, we should be able to do in our heads, since were supposed to
know our multiplication tables. However, to stick to the algorithm, well
find this first digit by subtraction, just as Gap taught me. The last digit subtracted, 3, is our first digit of the root.
After we find that first digit, the leading 3, we proceed to the next pair of
digits. Note, however, that we dont forget about the 3. This is an important
point, and quite unlike the case with long division. In long division, youll
recall, once a digit has been solved and subtracted, we dont use it anymore.
The digits of each column are processed separately. In the square root algorithm, on the other hand, the first digits of the solution are retained, and
appear in all subsequent subtractions.
Youll also note that, unlike division, our subtraction doesnt necessarily
remove all the higher digits. In general, the length of the remainder gets
longer and longer as we go, as does the number were subtracting.
76
Table 4.13
34
334
30 31
273
31 32
210
32 33
145
33 34
78
34 35
9
56
956
350 351
255
78
25578
3510 3511
18557
3511 3512
11534
3512 3513
4509
90
450990
35130 35131
380729
12
34
56
78
77
90
35131 35132
310466
35132 35133
240201
35133 35134
169934
35134 35135
99665
35135 35136
29394
As you can see from the table, Im still subtracting numbers in pairs, each
larger by one digit than the other. Note, however, that I dont ignore the
higher order digits, as I would in long division. Each of the subtracted numbers includes all the digits previously found, and Im working my way up
through the last digit, starting with zero. This process turned out to be just
the right approach for the Friden calculator, which had sticky digits and
thus remembered the leftmost digits already found. The nicest part of the
whole approach from the point of view of utility was that the numbers left
stuck in the keyboard after the last digit was found did, in fact, represent the
square root in this case
[4.24]
Its worth mentioning that this may be the only case in computing history
where the result of the computation is found entered into the keyboard,
instead of the result being displayed.
You might feel that Im still doing iterations here, but Im not. This is no
more an iterative process than its close cousin, long division, so it properly
deserves the title of a closed-form algorithm.
With shifting, I get a dramatic improvement in the number of computations. Instead of r passes through the loop, I need only the sum of all the digits in the result number, which must be less than 10 times the number of
digits. Now the algorithm is practical.
Another subtlety of the Friden algorithm is also worth mentioning: In
Table 4.13, I shifted the dividend by two digits to tackle the next digit of
the result. This meant copying the digits found thus far over one digit to the
78
n = 10m + b
[4.26]
n = 100a + 20ab + b
Ill assume that Ive already managed to find a by some method or other. Im
now looking for the next digit, b. Rewriting Equation [4.26] gives:
20ab + b = n 100a
2
b ( 20a + b ) = n 100a
or
2
[4.27]
79
100a - .
b = n------------------------20a + b
This formula gives a rule for finding b. Notice that the numerator of the
right-hand side is the original number, less the square of a, shifted left so
that the two line up. In short, the numerator is the remainder after Ive subtracted the initial guess for the root. To get the denominator, I must double
my guess a, shift it left one place, and add b.
At this point, if youve been paying attention, youre asking how you can
add b before you know what it is. The answer is that you cant. This makes
the square root algorithm a bit of a trial and error process, just like the division algorithm. In division, you also must guess at the quotient digit by
looking at the first couple of digits of the dividend and divisor. You cant be
sure that this guess is correct, however, until youve multiplied it by the
entire divisor and verified that you get a small, positive remainder.
In the same way, in the square root process, assume a value for b based
on a divisor of 20a then substitute the new value of b and make sure the
division still works.
It seems complicated, but its actually no more complicated than division,
and in some ways its a bit easier.
When I first wrote Equation [4.26], I treated a and b as though they were
single digits. However, youll note that theres nothing in the math that
requires that. I can now let a be all the digits of the root that Ive found so
far and let b be the one Im seeking next. In division, the trial quotient digit
must be tested and often adjusted at each step. The same is true in the
square root algorithm. However, in the latter, the larger a is, the less influence b will have on the square and the less likely youll have to backtrack
and reduce a. Therefore, although you must still always check to be sure,
theres actually less backtracking in the square root. The only thing that
makes the algorithm seem harder is that the roots, and therefore the differences, are getting larger as you go along, as shown in Table 4.13.
As an example, Ill use the same input value I used before: 1,234,567,
890. The first step is to point it off by twos.
12 34 56 78 90
80
As you can see, Ive brought down the next digits, just as I would in division, except I bring them down by twos, so the next dividend is 334. This is
the top half of the division in Equation [4.27].
The tricky part is remembering that the bottom half is not 10a + b, but
20a + b. Before I look for b, I must double the current root then tack on a
zero (bet thats the part you forgot). At this point I have
3
12 34 56 78 90 .
9
60 3 34
The division is 334/60, which yields five and some change. Before I write
down the new digit, however, I must make sure that the division still works
when I stick the five into the divisor. Now I have 334/65, which still yields a
five. Im OK, and I dont need to backtrack. I write this next digit down and
the trial root is now 35.
Its very important for you to see that the trial root and the number
you divide with are not the same because of the factor of two. The last
digit is the same, but the rest of the divisor is double that of the root.
81
What next? How do I subtract to get the new remainder? I cant just square
35 and subtract it, because Ive already subtracted the square of 3. Again,
Equation [4.26] provides our answer. I have
2
n = 100a + 20ab + b ,
b ( 20a + b ) = n 100a .
Ive also already performed the subtraction on the right; thats how I got
the remainder to divide with, to find b. Now that Ive found it, I must complete the subtraction by subtracting out the left-hand side. That is, the
remainder I now must obtain is
[4.28]
In this example, b = 5, and 20a + b = 65, so I subtract 325. This step is easier to do than to explain. Note that its identical to a division operation; that
is, I multiply the last digit found by the divisor (65 in this example), and
subtract it. After the subtraction, I have
3 5
12 34 56 78 90
9
.
65 3 34
3 25
9 56
At this point, you can begin to see how the algorithm works.
1.
2.
3.
4.
82
1 3 6
56 78 90
initial argument
square first digit
draw down, divide by 20*root
subtract 5*65
56
draw down, divide by 20*root
01
subtract 1*701
55 78
draw down, divide by 20*root
10 69
subtract 3*7023
45 09 90 draw down, divide by 20*root
42 15 96 subtract 6*70266
2 93 94 remainder
Note again that the last digit of the divisor must always be the same as the
multiplier when I subtract the next product. Thats because theyre both b.
Is the result correct? Yes. If you square 35,136, you get 1,234,538,496,
which is smaller than the original argument. If you square the next larger
number, 35,137, you get 1,234,608,969, which is too large. Thus the root I
obtained, 35,136, is indeed the largest integer whose square is less than the
original argument.
Youve just seen the high school algorithm in all its glory. Now that
youve seen it again, you probably recognize at least bits and pieces of it. If,
like me, youve tried to apply it in later years and found that you couldnt,
its probably because you forgot the doubling step. The thing that youre
dividing to get the next digit (the divisor) is not the root, nor is it exactly
twenty times the root. You get it by doubling the root, then appending a
zero. Also, before finally multiplying this divisor by the new digit, dont forget to stick that new digit into the divisor in place of the original zero. When
you do, theres always the chance that the new product is too large. In that
case, as in division, you must decrement the latest digit and try again.
Doing It in Binary
83
Doing It in Binary
I have now described how to find square roots by hand using decimal arithmetic. However, the algorithm is a lot more useful and universal than that.
In deriving it, I made no assumptions as to the base of the arithmetic, except
when I put the 10 in Equation [4.25]. Replace that 10 by any other base,
and the method still works. As in the case of division, its particularly easy
when I use base 2, or binary arithmetic. Thats because I dont have to do
division to get the next digit. Its either a 1 or a 0, so I either subtract or I
dont. I know how to test for one number greater than another, and thats as
smart as I need to be to make this method work in binary.
In the decimal case, youll recall that I made the first estimate of the quotient digit by using the divisor with a zero as the last digit. Then, once I had
the new digit, I stuck it into the divisor in place of the trailing zero and tried
again. Using binary arithmetic, I dont even have to do this step. If the division succeeds, it must succeed with the 1 as the last bit, so I might as well
put it there in the first place.
The example below repeats the long-form square root in binary. The
input argument is:
45, 765 = 0xb2c5 = 0b1011001011000101
84
1 1
10 11
1
101 1 11
1 01
1101
10
10
11001
1
110101
1101001
0 1 0 1 0 1
00 10 11 00 01 01
00
00
10
10
11
10
1
11010101
110101001
10
01
01
01
01
10
11
11
11
1
1
11
01
11
10
00
01
00
10
10
The root is 0b11010101, which is 0xd5, or decimal 213. This result tells me
that the square root of 45,765 is 213, which is true within the definition of
the integer root. The next larger root would be 214, whose square is 45,796,
which is too large. Any doubt you might have about the method is expelled
by looking at the remainder, which is
0b110001100 = 0x18c = 396 ,
Implementing It
Now that you see how the algorithm works, Ill try to implement it in software. The major difference between a hand computation and a computerized one is that its better to shift the argument so that Im always
subtracting on word boundaries. To do this, Ill shift the input argument
into a second word, two bits at a time. This second word will hold partial
remainders. Ill build up the root in a third word and the divisor in a fourth.
Remember, the divisor has an extra bit tacked on (the multiplication by
two) so I need at least one more bit than the final root. The easiest thing to
do is make both the divisor and remainder words the same length as the
input argument. Listing 4.6 shows the result. Try it with any argument from
Doing It in Binary
85
Better Yet
The code in Listing 4.7 looks hard to beat, but it uses a lot of internal variables. It needs one to hold the remainder, into which I shift the input argument, one to hold the root the program is building, and one to hold the
divisor. Can I do better?
I can. The secret is in noting the intimate relationship between the root
and the divisor. They differ only in that the root ends in 1 or 0, whereas the
corresponding divisor has a 0 tucked in front of the last bit and so ends in 01
or 00. You can see this relationship by comparing the two, as Ive shown in
Table 4.14 for the example problem. The divisors for successful as well as
unsuccessful subtractions are shown.
Table 4.14
Divisor
Successful?
101
yes
11
1101
no
110
11001
yes
1101
110101
no
11010
1101001
yes
110101
11010101
no
1101010
110101001
yes
Whether the subtraction is done or not, you can see that the divisor is
always four times the root plus one. The success or failure of the subtraction
only determines whether or not the bit inserted into the next root is a 1 or a 0.
Because theres always a predictable relationship between the root and
the divisor, I dont need to store both values. I can always compute the final
result from the last divisor. The binary algorithm, modified to use a single
86
Listing 4.7
An improved version.
unsigned short sqrt(unsigned long a){
unsigned long rem = 0;
unsigned long root = 0;
for(int i=0; i<16; i++){
root <<= 1;
rem = ((rem << 2) + (a >> 30));
a <<= 2;
root ++;
if(root <= rem){
rem -= root;
root++;
}
else
root--;
}
return (unsigned short)(root >> 1);
}
Conclusions
87
Conclusions
In this chapter, I described a super algorithm for fast floating-point square
roots that hacks the exponent to narrow the range and uses a linear function to give a first guess. This reduces the iterative process to only two or
three iterations. In the process, I discussed how to optimize functions over
their range of operation. This chapter also described three approaches to the
integer square root, each with its own strengths and weaknesses.
As you can see, theres much more to this simple function than meets
the eye. Its little wonder that it is poorly implemented so often. As you go
through the other algorithms in this book, youll find that they often have
much in common with the square root function. In particular, theres a lot
left unsaid when someone just hands you an equation, such as Equation
[4.1], and assumes that the problem has been solved. A proper implementation calls for a lot of attention to detail and consideration of things like the
kind of application, the needs for speed and accuracy, and the capabilities of
the target CPU. If the method is iterative, as Equation [4.1] requires, you
must concern yourself with how to perform the iteration, how to select the
starting value, and how to decide when to stop. Even if the method is a
closed form one, there are still a number of implementation details to work
out. If theres any one lesson to be learned from this chapter, it is this:
Never trust a person who merely hands you an equation.
88
5
Chapter 5
x = cos ( )
y = sin ( ) .
89
90
Figure 5.1
Sine/cosine definition.
r=1
y
0
X
It should be pretty obvious that the coordinates will change in proportion if you change the radius of the circle to some other value, r. In fact,
thats the main attraction of these trig functions, because I can then write
x = r cos ( )
and
[5.2]
y = r sin ( )
If I advance through its full range and plot the values of the two functions defined in Equation [5.1], I get the familiar sine waves shown in Figure
5.2. I need go no further than this figure to see a few important features of
the functions.
Both functions are bounded by values of +1.0 and 1.0.
Both functions repeat with a period of 360.
The sine function is odd, meaning that its reversed as it crosses zero.
[5.3]
sin ( x ) = sin ( x )
Figure 5.2
91
The cosine function is even, meaning that its reflected through the y axis.
[5.4]
cos ( x ) = cos ( x )
The cosine and the sine functions are identical except for a phase angle.
[5.5]
cos ( x ) = sin ( x + 90 )
When I was in high school, my textbook included trig tables with the values tabulated for all angles in steps of 0.1. I was taught to interpolate
between tabular values to get the results for the input angles needed.
Because I only needed values from 0 through 90, the tables had 900 entries
92
[5.7]
3
5
7
9
x
x - + ----x - ----x - + -----x ------sin x = x -----+
3! 5! 7! 9! 11!
[5.8]
10
You will find these definitions for these functions in any textbook or handbook on computer math. However, I hope you learned from the square root
example that a formula alone is not always enough to serve you in practical
applications.
Im sure you can see the patterns in the series. The signs of the terms
alternate, the powers are successive odd powers for the sine and successive
even ones for the cosine, and the denominator is the factorial of the integer
exponent in the numerator. (For those of you not familiar with the notation,
the ! means factorial, and n! is the product of all integers from 1 through
n.) The factorial function n! grows very rapidly as n increases, which is a
Good Thing, because it means that the series converge rapidly. As youll
soon see, however, rapidly can be a relative term, and not necessarily satisfactory in the real world.
For those who like to see things come to a satisfactory end, once you see
the pattern, its easy enough to write the general terms of the series (Equations [5.9]).
93
2n + 1
n x
sin n x = ( 1 ) --------------------( 2n + 1 )!
( n = 0 ,1 , )
[5.9]
2n
n x
cos n x = ( 1 ) ------------( 2n )!
It is these series that were used and still are to calculate the tables in
trig books. It may interest you to know that the generation of tables such as
these were the purpose behind Charles Babbages invention of the mechanical marvel he called the Difference Engine back in 1823. The Difference
Engine, which was never built, was supposed to compute functions to 20digit accuracy.
If you had a sufficiently large memory, you could take the path of those
old trig textbooks (and Babbage) and simply build a table of the functions.
But modern calculators and computers compute the functions, not to four
or five decimal places, but to anywhere from 10 to 20. That would take a
table of 45,000,000,000,000,000,000,000 entries not exactly practical,
even in todays climate of RAM extravagance. Instead, programmers are
faced with computing the values in real time as they are needed. This chapter explores the practical ramifications of this need.
Why Radians?
Theres a small gotcha in the series above: the angle x must be expressed
in radians. The radian, in turn, is defined such that there are 2 in a circle.
In other words,
1 circle = 2 radians = 360
so
1 radian = 360/(2) = 180/ = 57.29578.
94
As n increases in this series, the terms get smaller and smaller, but the
sum is infinite. Mathematicians must, and do, worry about the convergence
of power series; however, be assured that if someone gives you a series for a
function like sin(x), theyve already made sure it converges. In the case of
the sine and cosine functions, the alternating signs make sure that small
terms do not add up to large numbers.
If higher order terms become negligible, you can truncate the series to
obtain polynomial approximations. You must always do this with any infinite series. Sometimes you can estimate the error that has been introduced,
but most often programmers simply keep enough terms to be sure the error
stays within reasonable bounds.
It might be helpful to take a look at an example. Table 5.1 computes the
sine and cosine for x = 30 or /6 radians. Within the limits of accuracy
shown, both series have converged after the fifth term (the cosine is always a
little slower because its terms have one less power of x in them, term for
term). As promised, the sizes of the terms decrease rapidly thanks to the factorial in the denominator.
95
Term Limits
The number of terms required for the sine or cosine series to converge
depends on the size of the angle. The infinite series are guaranteed to converge for all values of x, but there are no guarantees as to when. In my
example, I deliberately chose a fairly small angle, 30, for the input argument. Thats the reason for rapid convergence. Suppose, instead, I try the
value x = 1,000. Then the numerators of the terms in Equation [5.7] will be
1000, 109, 1015, 1021, and so on. It will take a very large number of terms
before the factorials begin to assert their authority over numbers like that.
In the practical sense, it will never happen, because Im likely to get floatingpoint overflows (using single-precision floating point, at least). Even if I
didnt, the final answer would be buried under round off error, which is generated by subtracting nearly equal terms.
Table 5.1
Term
1
2
3
4
5
6
Sine
0.523598776
0.499674180
0.500002133
0.499999992
0.500000000
0.500000000
Cosine Term
1.000000000
.137077839
0.003131722
.000028619
0.000000140
0.000000000
Cosine
1.000000000
0.862922161
0.866053883
0.866025264
0.866025404
0.866025404
The end result for any practical computation of the trig functions is that
I must limit the range of x, just as I did for the square root function. Clearly,
the smaller the range, the fewer terms Ill need. I havent shown you yet how
to limit the range, but assume for the moment that it can be done. How
many terms will be required?
In this case, its fairly straightforward to compute how many terms Ill
need. Because the terms in the series have alternating signs, I neednt worry
about sneaky effects such as lots of small numbers summing up to a larger
one. As you saw in Table 5.1, the intermediate results oscillate around the
final result. Because of this, I can be assured that the error will always be
smaller (sometimes a lot smaller) than the size of the first neglected term. All
I need to do is to compute the values of each term in the equations for representative values of the angular range. I can then compare the results with
the resolution of the least significant bit (LSB) in the numeric representation
96
Table 5.2
Term
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
180
3.1411
5.1677
2.5502
0.5993
0.0821
0.0074
4.7e4
2.2e5
8.0e7
2.3e8
5.4e10
1.1e11
1.7e13
2.4e15
3.0e17
3.1e19
2.9e21
90
1.5708
0.6460
0.0797
0.0047
1.6e4
3.6e6
5.7e8
6.7e10
6.1e12
4.4e14
2.6e16
1.3e18
5.2e21
45
0.7854
0.0807
0.0025
3.7e5
3.1e7
1.8e9
4.3e12
1.3e14
2.9e17
5.1e20
30
0.5236
0.0239
0.0003
2.1e6
8.2e9
2.0e11
3.6e14
4.7e17
4.7e20
22.5
0.3927
0.0101
7.8e5
2.9e7
6.1e10
8.6e13
8.5e16
6.2e19
3.5e22
15
0.2618
0.0030
1.0e5
1.7e8
1.6e11
9.9e15
4.4e18
1.4e21
11.25
0.1963
0.0013
2.4e6
2.2e9
1.2e12
4.2e16
1.0e19
1.9e23
97
98
Table 5.3
Term
1
2
3
4
5
6
7
8
9
10
11
12
13
180
1.0000
4.9348
4.0578
1.3353
0.2353
0.0258
0.0019
1.0e4
4.3e6
1.4e7
3.6e9
1.1e11
7.7e11
90
1.0000
1.2337
0.2537
0.0209
9.2e4
2.5e5
4.7e7
6.4e9
6.6e11
5.3e13
3.4e15
1.8e17
8.2e20
45
1.0000
0.3084
0.0159
3.3e4
3.6e6
2.5e8
1.2e10
3.9e13
1.0e15
2.0e18
3.3e21
30
1.0000
0.1371
0.0031
2.9e5
1.4e7
4.3e10
8.9e13
1.3e15
1.5e18
1.4e21
22.5
1.0000
0.0771
0.0010
5.1e6
1.4e8
2.4e11
2.8e14
2.4e17
1.5e20
15
1.0000
0.0343
2.0e4
4.5e7
5.5e10
4.2e13
2.2e16
8.1e20
11.25
1.0000
0.0193
6.2e5
8.0e8
5.5e11
2.3e14
6.9e18
1.5e21
99
x
Term
14
15
16
17
180
2.1e14
2.7e16
3.1e18
3.1e20
90
45
30
22.5
15
11.25
I can improve things considerably by noting that each term can be computed from the preceding one. Ill go back to the general term for the sine
function given in Equation [5.9].
2n + 1
n x
sinn x = ( 1 ) --------------------( 2n + 1 )!
n+1
[5.10]
= ( 1 )
Table 5.4
n+1
2(n + 1) + 1
x
-----------------------------------( 2 ( n + 1 ) + 1 )!
2n + 3
x
---------------------( 2n + 3 )!
180
6
7
9
12
13
15
16
90
4
5
7
9
10
11
12
45
2
4
5
7
8
9
10
30
2
3
4
6
7
8
9
22.5
2
3
4
6
6
7
8
15
2
2
3
5
6
6
7
11.25
1
2
3
4
5
6
7
100
x
--------------------( 1 )
sin n + 1x
( 2n + 3 )!
------------------- = -----------------------------------------2n + 1 sinn x
n x
( 1 ) --------------------( 2n + 1 )!
n+1
x ( 2n + 1 )!- .
= -----------------------------( 2n + 3 )!
[5.11]
x
R n + 1 = ----------------------------------------.
( 2n + 3 ) ( 2n + 2 )
Similarly,
[5.13]
Table 5.5
z
z
z
z
cos x = 1 --z- 1 ------ 1 ------ 1 ------ 1 ------ ( 1 )
2
12
30
56
90
180
6
8
10
90
4
6
7
45
4
4
5
30
2
4
4
22.5
2
3
4
15
2
3
4
11.25
2
3
4
101
x
Bits
40
48
56
64
180
13
14
15
16
90
9
11
11
13
45
7
8
9
10
30
7
7
8
9
22.5
6
7
8
8
15
5
6
7
7
11.25
5
6
7
7
This formulation is called Horners method, and its just about the optimal way to calculate the series. Note that the denominators at each step are
the products of two successive integers in the sine, 2*3, 4*5, 6*7, and so
on. Once you spot this pattern, youll be able to write these functions from
memory. If you dont have access to a canned sine function, and you need
one in a hurry, direct coding of these two equations is not bad at all.
There are still a couple of details to iron out. First, I have a small problem: the method as shown uses a lot of stack space because of all the intermediate results that must be saved. If your compiler is a highly optimizing
one, or if you dont care about stack space, you have no problem. But if
youre using a dumb compiler and an 80x87 math coprocessor, you will
quickly discover, as I did, that you can get a coprocessor stack overflow.
Listing 5.1
The old Hewlett-Packard (HP) RPN (reverse Polish notation) calculators had only four stack locations. Those of us who used them heavily
learned to always evaluate such complicated expressions from the inside
102
This is the best form to use when using a pocket calculator. Its easy to
remember, and it makes effective use of the CHS (+/-) key. But for a computer implementation, I can eliminate half of the sign changes by yet
another round of transformations. It may not be obvious, but if I carefully
fold all the leading minus signs into the expressions in parentheses, I can
arrive at the equivalent forms.
[5.15]
[5.16]
This is about as efficient as things get. The equations are not pretty, but they
compute really fast. Just as I can add more terms on the right in Equations
[5.12] and [5.13], I can add them on the left in Equations [5.15] and [5.16].
Just remember that this time the signs alternate, beginning with the plus
signs shown at the right end.
A direct implementation of these equations is shown in Listing 5.2. The
number of terms used are appropriate for 32-bit floating-point accuracy and
45 range. Because most computers perform multiplication faster than
division, Ive tweaked the algorithms by storing the constants as their reciprocals (Dont worry about the expressions involving constants; most compilers will optimize these out). Notice the prepended underscore to the
names. This is intended to underscore (pun intended) the fact that the functions are only useful for a limited range. Ill use these later in full-range versions.
Listing 5.2
Horners method.
// Find the Sine of an Angle <= 45
double _sine(double x){
double s1 = 1.0/(2.0*3.0);
double s2 = 1.0/(4.0*5.0);
double s3 = 1.0/(6.0*7.0);
Listing 5.2
103
Horners method.
double s4 = 1.0/(8.0*9.0);
double z = x * x;
return ((((s4*z-1.0)*s3*z+1.0)*s2*z-1.0)*s1*z+1.0)*x;
}
// Find the Cosine of an Angle <= 45
double _cosine(double x){
double c1 = 1.0/(1.0*2.0);
double c2 = 1.0/(3.0*4.0);
double c3 = 1.0/(5.0*6.0);
double c4 = 1.0/(7.0*8.0);
double z = x * x;
return (((c4*z-1.0)*c3*z+1.0)*c2*z-1.0)*c1*z+1.0;
}
The second detail? Ive quietly eliminated a test for deciding how many
terms to use. Since you must compute Horners method from the inside out,
you must know where inside is. That means you cant test the terms as
you go; you must know beforehand how many terms are needed. In this
case, the extra efficiency of Horners method offsets any gains you might
achieve by skipping the computation of the inner terms, for small x. This
result parallels an observation from the discussion of square roots in Chapter 4: sometimes its easier (and often faster) to compute things a fixed number of times, rather than testing for some termination criterion. If it makes
you feel better, you can add a test for the special case when x is very nearly
zero; otherwise, its best to compute the full expression.
You may be wondering why Ive included functions for both the sine and
cosine, even though Ive already shown In Equation [5.5] that the cosine can
be derived from the sine. The reason is simple: you really need to be able to
compute both functions, even if youre only interested in one of them. One
approximation is most accurate when the result is near zero and the other
when its near one. Youll end up using one or the other, depending on the
input value.
104
This at least protects me from foolish inputs like 10,000. Note that the
result of this operation should always yield a positive angle, in the range 0
to 2.
Second, the sine function is odd, so
[5.18]
sin ( x + ) = sin ( x ) .
If the angle is larger than 180 ( radians), I need only subtract then
remember to change the sign of the result.
Third, the sine and cosine functions are complementary (see Equation
[5.5]), so
[5.19]
sin ( x + 2 ) = cos ( x ) .
Using this gets me the range 0 to 90 (/2 radians). Finally, I can use the
complementary nature again in a little different form to write
[5.20]
If the angle is larger than 45 (/4 radians), I subtract it from 90, yielding
an angle less than 45. This transformation serves two purposes: it reduces
the range to 0 to 45 and it guarantees that I use whichever of the two series
(Equation [5.15] or [5.16]) that is the most accurate over the range of interest.
All of this is best captured in the pseudocode of Listing 5.3. Its straightforward enough, but rather busy. Ive actually seen the sine function implemented just as its written here, and even done it that way myself when I
was in a hurry. But the nested function calls naturally waste computer time.
More than that, the code performs more sign changes and angle transformations than are really needed. Even the real-variable mod() function is
105
Listing 5.3
Note the name change in the base functions; to distinguish them from the
full-range functions, Ive prepended an underscore to their names.
106
A Better Approach
Fortunately, theres a very clean solution that eliminates all the problems,
even the need for mod(). Part of the solution hinges on the fact that I dont
really have to limit the angle to positive values. A look at Equations [5.7]
and [5.8] show that theyre equally valid for negative arguments.
Figure 5.3
Range reduction.
cos x90
sin x180
cos x90
1
2
2
sin x180
sin x
0
3
cos x270
sin x
cos x270
For the rest of the algorithm, look at Figure 5.3. It might seem at first
glance that the unit circle is divided into its eight octants. However, a closer
look reveals only four 90 quadrants, each centered about the x and y axes.
The quadrants are rotated 45 counterclockwise from their usual positions.
Within each quadrant, the formula needed to compute the sine of the angle
is shown.
Ill assign each quadrant a number, zero through three, as shown. This
number can be very quickly calculated if I use rounding instead of truncation to get the integer.
n = Round ( x 90 )
I can then reduce the angle to its essence by subtracting the appropriate
value.
x = x 90n
Note that this formula will reduce any angle, positive or negative, to the range
45 to 45. The quadrant number may then be found by taking n mod 4.
107
A simple case statement now gives the result I need for every case. The
resulting code is shown in Listing 5.4. Note that I dont need a separate set
of computations to compute the cosine. I merely add 90 (/2 radians) to the
angle and call the sine. Using this approach, I end up handling both mod()
and the angle reduction in a single step and with a minimum of sign changes
and logic.
Listing 5.4
Tightening Up
That range reduction to an angle less than 45 was pretty easy. From
Tables 5.4 and 5.5, you can see that Ill need nine terms to get double-precision accuracy. Id like to reduce the number of terms even further, but the
obvious next question is: Can I? Answer: Yes, but not without a price.
All of the range reductions can be derived from a single trig identity.
[5.21]
108
Table 5.6
60n
0
60
120
180
240
300
cos(60n)
1.00000
0.50000
0.50000
1.00000
0.50000
0.50000
sin(60n)
0.00000
0.86603
0.86603
0.00000
0.86603
0.86603
sin(x + 60n)
sin(x)
0.5 sin(x) + 0.86603 cos(x)
0.5 sin(x) + 0.86603 cos(x)
sin(x)
0.5 sin(x) 0.86603 cos(x)
0.5 sin(x) 0.86603 cos(x)
The good news from these tables is that, at worst, I have to store and use
a couple of multiplicative constants. With steps of 30, one of the constants
is 0.5, which can be accomplished by a shift if Im using integer arithmetic.
The 45 case (22.5) is especially appealing because the same constant multiplies both parts when there are two parts to compute.
The bad news is I have two parts to compute in half the cases. This
means I must calculate both the sine and cosine via the series. Because the
range has been reduced, I need fewer terms in the series (see Tables 5.4 and
5.5). Still, it turns out that the total number of multiplies required will
always be higher than for the four-quadrant case. As long as I only need a
sine or a cosine, I gain nothing in speed by limiting the range to less than
45.
Table 5.7
45n
0
45
90
135
180
225
270
315
cos(45n)
1.00000
0.70711
0.00000
0.70711
1.00000
0.70711
0.00000
0.70711
sin(45n)
0.00000
0.70711
1.00000
0.70711
0.00000
0.70711
1.00000
0.70711
sin(x + 45n)
sin(x)
0.70711(sin(x) + cos(x))
cos(x)
0.70711(sin(x) cos(x))
sin(x)
0.70711(sin(x) + cos(x))
cos(x)
0.70711(sin(x) cos(x))
Table 5.8
109
30n
0
30
60
90
120
150
180
210
240
270
300
330
cos(30n)
1.00000
0.86603
0.50000
0.00000
0.50000
0.86603
1.00000
0.86603
0.50000
0.00000
0.50000
0.86603
sin(30n)
0.00000
0.50000
0.86603
1.00000
0.86603
0.50000
0.00000
0.50000
0.86603
1.00000
0.86603
0.50000
sin(x + 30n)
sin(x)
0.86603 sin(x) + 0.5 cos(x)
0.5 sin(x) + 0.86603 cos(x)
cos(x)
0.5 sin(x) + 0.86603 cos(x)
0.86603 sin(x) + 0.5 cos(x)
sin(x)
0.86603 sin(x) 0.5 cos(x)
0.5 sin(x) 0.86603 cos(x)
cos(x)
0.5 sin(x) 0.86603 cos(x)
0.86603 sin(x) 0.5 cos(x)
z- 1 z + 1 x
sin x = ---- 20 --6
[5.23]
z- 1 --z- + 1
z- + 1 ----cos x = ---- 30
12 2
This is starting to look much simpler. Because these functions are so short, I
can speed things up even more by inserting them as inline functions.
110
Listing 5.5
Both at once.
// Find both the sine and cosine of an angle
void sincos(double x, double *s, double *c){
double s1, c1;
long n = (long)(x/pi_over_six + 0.5);
x -= (double)n * pi_over_six;
n = n % 12;
if(n < 0)
n += 12;
double z = x*x;
double s1 = ((z/20.0-1.0)*z/6.0+1.0)*x;
double c1 = ((z/30.0+1.0)*z/12.0-1.0)*z/2.0+1.0;
switch(n){
case 0:
s = s1;
c = c1;
break;
case 1:
s =
c1;
c = -s1;
break;
case 4:
s = -sin_30 * s1 + cos_30 * c1;
c = -cos_30 * s1 - sin_30 * c1;
break;
case 5:
s = -cos_30 * s1 + sin_30 * c1;
c = -sin_30 * s1 - cos_30 * c1;
break;
case 6:
s = -s1;
c = -c1;
break;
case 7:
s = -cos_30 * s1 - sin_30 * c1;
c =
break;
case 8:
s = -sin_30 * s1 - cos_30 * c1;
c =
break;
case 9:
s = -c1;
c =
s1;
break;
case 10:
s =
c =
break;
111
112
case 11:
s =
c =
break;
}
}
Table 5.9
n
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cos(22.5n)
1.00000
0.92388
0.70711
0.38268
0.00000
0.38268
0.70711
0.92388
1.00000
0.92388
0.70711
0.38268
0.00000
0.38268
0.70711
0.92388
sin(22.5n)
0.00000
0.38268
0.70711
0.92388
1.00000
0.92388
0.70711
0.38268
0.00000
0.38268
0.70711
0.92388
1.00000
0.92388
0.70711
0.38268
sin(x + 22.5n)
sin(x)
0.92388 sin(x) + 0.38268 cos(x)
0.70711 sin(x)
0.38268 sin(x) + 0.92388 cos(x)
cos(x)
0.38268 sin(x) + 0.92388 cos(x)
0.70711(sin(x) cos(x))
0.92388 sin(x) + 0.38268 cos(x)
sin(x)
0.92388 sin(x) 0.38268 cos(x)
0.70711(sin(x) + cos(x))
0.38268 sin(x) 0.92388 cos(x)
cos(x)
0.38268 sin(x) 0.92388 cos(x)
0.70711(sin(x) cos(x))
0.92388 sin(x) 0.38268 cos(x)
113
the output values are bounded. Ill illustrate the procedures below assuming
16-bit arithmetic, but the results can be extended easily to other word
lengths.
BAM!
For starters, theres a neat trick you can use to represent the input angle.
Instead of using degrees or radians, you can use a different scale factor. The
best choice is the so-called Binary Angular Measure (BAM), in which the
angle is measured in pirads. This quantity, as the name implies, stands for
an angle of radians; that is,
1 BAM = 1 pirad = radians = 180.
114
Table 5.10
Hex
Segment (4)
0000
30
1555
45
2000
60
2AAB
90
4000
120
5555
135
6000
150
6AAB
180
8000
210 = 150
9555
225 = 135
A000
10
240 = 120
AAAB
11
270 = 90
C000
12
300 = 60
D555
13
315 = 45
E000
14
330 = 30
EAAB
15
360 =
0000
Segment (16)
The use of BAM presents another huge freebie: I no longer have to use
modulo tricks to identify the quadrant numbers. As long as I stick to dividing the circle into 4, 8, or 16 segments that is, powers of two the
quadrant number is easy to get from the high bits of the angle. As you can
see in Table 5.10, the segment number for the 16-segment case is simply the
high hex digit, rounded. Similarly, for four segments, its the high two bits,
rounded. This makes the range reduction logic easy. After computing the
segment number n, I can reduce the angle simply by subtracting n again,
appropriately shifted left. Because of the rounding, the result could be either
positive or negative.
More specifically, the algorithm for finding the segment numbers is
n = ( x + 0x2000 ) 14
or
n = ( x + 0x800 ) 12
115
Representing the function result is a little bit trickier. In floating point, the
sine has a range of 1.0 to +1.0. My 16-bit integer has a range of 32,768 to
+32,767. Notice that it is not symmetrical; I am one bit short of being able
to represent the result with maximum resolution. In other words, if I define
the binary point to be at the far left of the word, I can represent the 1, but
not the +1.
I have two options: I can say thats close enough, and simply let 7FFF
be one, or I can bite the bullet and lose one bit of accuracy to get the
proper scaling. I prefer to take the former choice, to give me as many bits of
accuracy as possible. With short integers, I need all the bits I can get. To
keep me out of trouble with asymmetries, Ill resolve to limit the function
symmetrically for both positive and negative integers. Thus Ill define the
integer sine function as:
[5.24]
But Ill artificially limit the extreme values to 1. This function gives 15 bits
of resolution.
If I were using 32-bit integers, Id probably take the opposite tack, and
waste one bit to get an exact representation of the value 1. That is, Id let
1.0 be represented by 0x40000000, and not worry about the next-to-highest
bit that never gets used.
Only the very critical issue of scaling remains. When youre working
with integers, its important that all coefficients and intermediate results are
scaled to get the maximum resolution at every step, without overflow. Also,
dont forget that the argument in the series is supposed to be expressed in
radians, but input is measured in pirads. To get things right again, you have
to fold the conversion factor into each term. You could simply convert the
argument back to radians before you start, but it turns out that you can get
better scaling by taking a different approach. For integer arithmetic, things
work better if all the coefficients are roughly the same size, and you can get
that effect by a different factoring.
Lets go back to the form of Equations [5.7] and [5.8]:
3
11
[5.7]
[5.8]
10
116
[5.25]
z = y ,
and factor out only the powers of z, not the coefficients. Youll get
7
[5.26]
[5.27]
Table 5.11
Numeric Value
Hex Value
s1 =
3.141592654
$6488
s2 = 3/3!
5.167712782
$2958
5/5!
2.550164042
$051A
s4 = 7/7!
0.599264530
$004D
c0 = 1
1.000000000
$7FFF
c1 = 2/2!
4.934802202
$4EF5
c2 = 4/4!
4.058712128
$103E
c3 = 6/6!
1.335262770
$0156
s3 =
Notice how nearly equal these coefficients are. In this case, thats a blessing because it helps me avoid most of the shifts normally needed to keep
117
enough significant digits. What shifting I need is pretty much taken care of
by the multiplications and fixed-point scaling. This scaling is based on a
couple of assumptions. First, the input value (0x2000 pirads at its largest)
will be shifted left one bit for more significance. Second, the value of z will
be shifted left two bits after its computed.
If I put all this together and Hornerize it one more time, I get the algorithm shown in Listing 5.6. Here Im showing the code in C, but you should
think of this as mere pseudocode. Since the goal of the integer versions is
speed, Im assuming that youd actually implement the functions in assembly language.
The many right shifts and type-casts are a consequence of using integer
arithmetic to represent left-adjusted real numbers.
Normally, in fixed-point arithmetic, we try to adjust the scale of all
parameters to maintain as many non-zero bits as possible. In short, we tend
to normalize them to the left. In Table 5.11, you will see that Ive violated
this rule. In this case, the exception is justified because the higher-order
terms only contribute slightly to the final result. Ive chosen the scaling of
the coefficients very carefully, to ensure maximum accuracy while retaining
efficiency. The final algorithms are within 1 bit over the entire range,
which is about all one can hope for using integer arithmetic. If youre willing to accept the extra bother of rounding after each multiplication (shifting
only truncates), you can make the algorithms accurate within 1/2 bit,
which is exact within the limits of our chosen word length. To round, shift
right one less bit than shown in the code, add one to that last bit, then shift
right one more time.
Note that, even though the input argument, y, is never larger than
0x2000, Ive made no attempt to normalize it left. Remember that y is measured in pirads, which uses all 16 bits to measure angles. We already have
all the precision the format allows. Shifting left buys you nothing; youd
only be shifting in meaningless zeros.
On the other hand, the square z = y2 has more significant bits, and we
can retain a few of these by shifting the product right a few bits fewer than
normal. The scaling of z is reflected in that of the coefficients. One happy
consequence of my choices of scaling is that most of the right shifts are 16
bits. This wont make much difference if your CPU has a barrel-shifter, but
it makes a world of difference for the small CPUs that dont, because it
means that you can effect the right shift simply by addressing only the
upper half of the 32-bit product.
118
Listing 5.6
Integer sine/cosine.
short function _sin(short y){
static short s1 = 0x6488;
static short s3 = 0x2958;
static short s5 = 0x51a;
static short s7 = 0x4d;
long z, prod, sum;
z = ((long)y * y) >> 12;
prod = (z * s7) >> 16;
sum = s5 prod;
prod = (z * sum) >> 16;
sum = s3 prod;
prod = (z * sum) >> 16;
sum = s1 prod;
// for better accuracy, round here
return (short)((y * sum) >> 13);
}
Chebyshev It!
119
; note, not 16
Chebyshev It!
An article about sines and cosines wouldnt be complete without some mention of the use of Chebyshev polynomials. Basically, the theory of Chebyshev polynomials allows the programmer to tweak the coefficients a bit for
a lower error bound overall. When I truncate a polynomial, I typically get
very small errors when x is small, and the errors increase dramatically and
exponentially outside a certain range near x = 0. The Chebyshev polynomials, on the other hand, oscillate about zero with peak deviations that are
bounded and equal. Expressing the power series in terms of Chebyshev
polynomials allows you to trade off the small errors near zero for far less
error near the extremes of the argument range. I will not present a treatise
on Chebyshev polynomials here; for now, Ill only give the results of the
process.
You dont need to know how this is done for the purposes of this discussion, but the general idea is to substitute every power of x by its equivalent
in terms of Chebyshev polynomials, collect the terms, truncate the series in
that form, and substitute back again. When all the terms have been collected, youll find that you are back to a power series in x again, but the
coefficients have been slightly altered in an optimal way. Because this process results in a lower maximum error, youll normally find you can drop
one term or so in the series expansion while still retaining the same accuracy.
Just so you dont have to wait for the other shoe to drop, the Chebyshev
version of the four-quadrant coefficients is shown in Table 5.12. Note that I
need one less term in both functions.
120
Table 5.12
Numeric Value
Hex Value
s1
3.141576918
$6487
s3
5.165694407
$2953
s5
2.485336730
$04F8
c0
0.999999842
$7FFF
c2
4.931917315
$4EE9
c4
3.935127589
$0FBD
The implementations shown here are about as efficient as you can hope to
get. As you can see by what I went through to generate practical software,
theres a lot more to the process than simply implementing the formulas in
Equations [5.7] and [5.8] blindly. As was the case with square roots, the
devil is in the details.
x 2 x 1 = x .
If youre seeking sin(x) for a value in between these two, the interpolation
formula is
[5.29]
121
yn + 1 yn
y(x) = y ( x n ) + ( x x 1 ) ---------------------xn + 1 xn
( x + x1 )
- ( y 2 y 1 ).
= y 1 + -----------------x
( x x1 )
e(x) = (x) y(x) = (x) y 1 + -----------------( y2 y1 ) .
x
So far, things look pretty messy. The nature of the error function is certainly
not intuitively obvious at this point. However, consider the error at the two
endpoints.
( x1 x 1 )
e(x 1) = (x 1) y 1 + -------------------- ( y2 y1 )
x
= (x 1) y 1
Similarly,
[5.31]
( x2 x1 )
e(x 2) = f (x 2) y 1 + -------------------- ( y2 y1 )
x
= f ( x 2) [ y 1 + ( y 2 y 1 ) ]
= (x 2) y 2 .
If the tabular values are correct, the value for y1 is in fact (x1), and ditto for
y2, so
[5.32]
e(x 1) = e(x 2) = 0 .
When you stop and think about it, this result should hardly be a surprise.
Ive chosen the tabular points to yield values of (x) at the tabular values of
x, so the interpolation should give exact results for any value that happens
to lie on a tabular point. As I progress from one tabular point to another, I
must assume that the error climbs from zero to some peak value then
shrinks back to zero again. Where is the peak value? I dont know for sure
only calculus can answer that question but a very good guess would
be that its close to the midpoint.
122
[5.33]
x = 1--- ( x 1 + x 2 )
2
= 1--- ( x 1 + x 1 + x )
2
-----x = x 1 + x
2
[5.34]
x- x
1 ( y 2 y 1 )
e max = x 1 + x
------ y 1 + x 1 + ----2
---------------------------------2
x
------ y 1 + 1--- ( y 2 y 1 )
= x 1 + x
2
2
------ 1--- ( y 1 + y 2 )
e max = x 1 + x
2 2
or
[5.35]
x 1
e max = x 1 + ------ --- [ ( x 1 ) + ( x 1 + x ) ] .
2 2
So far, I have not made use of the nature of (x); the formula above is
good for any function. Now Ill specialize it to the sine function.
[5.36]
x 1
e max = sin x 1 + ------ --- [sin(x1) + sin(x1 + x)]
2 2
Clearly, the value of this error depends on the choice of x; I can expect the
maximum error between tabular points to vary over the range of x. Im now
going to assert without proof that the maximum of the maximum error
call it E occurs at 90, or /2 radians. Its easy enough to prove this
rigorously, but an arm-waving argument is satisfying enough here; it stands
to reason that the error in a linear interpolation is going to be largest when
the curvature of the function is greatest. For the sine function, this is the
case when x = /2.
The math will come out easier if I assume that x1 and x2 straddle this
value, so that
[5.37]
------ =
--x 1 + x
2
2
--- x
-----x1 =
2 2
------ .
--- + x
x2 =
2 2
= 1.
------ = sin
sin x 1 + x
--2-
2
[5.40]
------ .
sin ( x 1 ) = cos x
2
Similarly,
[5.41]
------ .
sin ( x 2 ) = cos x
2
------ .
E = 1 cos x
2
123
124
so
------
E = 1 1 1--- x
2 2
or
2
[5.43]
--------- .
E = x
8
I now have the relationship I need to estimate what the error will be for any
spacing, x, between tabular points. More to the point, now I can compute
how many table entries Ill need, using linear interpolation, to get any
desired accuracy. To be useful, any table must cover the range 0 to 45, or 0
to /4 radians. (You can get the rest of the range using the angle conversion
formulas in Equations [5.17] to [5.20]). The number of table entries must be
roughly
[5.44]
-.
N = --------4x
x =
8E ,
or approximately
[5.46]
------------- .
N = 0.278
E
125
Table 5.13
short integer
215
50
long integer
231
12,882
24-bit float
105
50
32-bit float
107
880
36-bit float
108
2,780
40-bit float
1010
27,800
64-bit float
1017
8.8e7
80-bit float
1019
8.8e8
One last point remains to be made. Remember those little boxes in your
trig books titled proportional parts? In case youve forgotten what they
were, these were the slopes of the function in the range covered by that
page. In short, they were the values of the expression
[5.47]
y2 y1
-.
m = --------------x
126
x-
n = floor ---- x
u = mod (x, x) .
Using this approach, you can find both the integer (index) part and the
fractional part of the input argument in two simple calculations, and if
youve also stored the slope as suggested, you end up with a very fast algorithm.
The foregoing comments are even more appropriate if you use the BAM
protocol for storing the angle. In this case, you can get both parts (integer
and fraction) of the angle simply by masking the binary number representing it. The high bits give the index into the table and the low bits the part
to be used in interpolation. The end result is an algorithm that is blazingly
fast far more so than the power series approach. If your application can
tolerate accuracies of only 16 bits or so, this is definitely the way to go.
6
Chapter 6
Arctangents:
An AngleSpace Odyssey
When youre working with problems that involve trigonometry, youll find
that about 80 percent of your work will be occupied with computing the
fundamental functions sine, cosine, and tangent of various angles. Youll
find the infinite series that give these transformations in almost any handbook. From my totally unbiased point of view, one of the more comprehensive treatments for the first two functions was just given in Chapter 5. The
third function, the tangent, is relatively easy because its related to the first
two by the simple relation
[6.1]
sin x
tan ( x ) = ----------- .
cos x
This function works for all angles except those for which the cosine is
zero. At that point, the definition blows up, and the magnitude of the tangent goes to infinity. Note that this problem cannot be solved with tricky
coding; the function itself blows up, so theres no way fancy coding will stop
it. However, you can keep the explosion within bounds, which is what I
did in Listing 3.2 (see the section Is It Safe?).
127
128
129
As you can see in the graph of the function shown in Figure 6.2, its not
exactly well behaved or bounded. Whereas the sine and cosine can have values only within the range of 1.0, the tangent exceeds 1.0 for angles greater
than 45 and, in fact, wanders off the graph to infinity as approaches 90,
only to show up again coming in from negative infinity beyond 90. Still, as
strange as the function is, you can find its value for any angle except 90.
You can conceptually find its tangent by looking it up on the graph: draw a
vertical line for the desired angle, and the tangent is the corresponding value
where this line crosses the curve.
Figure 6.1
Tangent definition.
y=tan 0
r
0
x=1
The same approach can be used in reverse: to find the arctangent, draw a
horizontal line corresponding to a given value of the tangent; the desired
angle is given by the point where this line crosses the tangent curve. (It
might help if you turn the page sideways!) The good news is that the arctangent is well behaved and defined even for those infinite values. The bad
news is that you have to deal with those infinities! You cant use a graphical
approach with a computer. You need an equation, and youll find it in the
handbooks in the form of yet another infinite series, as given in Equation
[6.2].
3
[6.2]
11
1
x - + ---x - ---x - + ---x - -----x -+
tan x = x ---3 5 7 9 11
130
[6.3]
Figure 6.2
11
That teensy difference, however, makes all the difference. The exclamation point stands for the factorial function, and as Im sure you know, the
value of this function increases extremely rapidly as its argument increases.
Because it appears in the denominators of the fractions in Equation [6.3], it
guarantees that the series converges rapidly for all values of x. Even when x
is huge and its powers are increasing rapidly with each higher term, the factorial will eventually overwhelm the numerator, and the terms approach
zero.
By contrast, the convergence of the series for the arctangent is painfully
slow, and it doesnt converge at all for values of |x| > 1. A picture is worth a
thousand words, and in this case, you certainly cant grasp the enormity of
131
the effect of that factorial sign without seeing it in action. Table 6.1 shows
the value of each term in the arctangent series through the first 20 terms for
the limiting value of 45, where the tangent is equal to 1.0. The correct
answer should be 45 expressed in radians, or 0.785398163.
At first glance at Table 6.1, the series appears to work well. After the first
few terms, the answer seems to be closing in on the exact value. However,
by the 20th term, you can see that something is badly wrong, because things
are not improving very much. The error in the 20th term is only half that of
the 10th. You can see that although the error is indeed decreasing (the
series, after all, does converge), it is decreasing much too slowly. For a dramatic comparison, see the performance of the sine series shown in Table 6.2
and Figure 6.3.
Table 6.1
Term
Sum
Error
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1
0.333333333
0.2
0.142857143
0.111111111
0.090909091
0.076923077
0.066666667
0.058823529
0.052631579
0.047619048
0.043478261
0.04
0.037037037
0.034482759
0.032258065
0.03030303
0.028571429
0.027027027
0.025641026
1
0.666666667
0.866666667
0.723809524
0.834920635
0.744011544
0.820934621
0.754267954
0.813091484
0.760459905
0.808078952
0.764600691
0.804600691
0.767563654
0.802046413
0.769788349
0.800091379
0.77151995
0.798546977
0.772905952
0.2146
0.118731
0.08127
0.061589
0.04952
0.041387
0.03554
0.03113
0.02769
0.024938
0.02268
0.020797
0.0192
0.017835
0.01665
0.01561
0.01469
0.013878
0.01315
0.012492
132
Table 6.2
Figure 6.3
Term
Sum
Error
1
2
3
4
5
6
7
8
9
1
0.166666667
0.008333333
0.000198413
2.75573e06
2.50521e08
1.6059e10
7.64716e13
2.81146e15
1
0.833333333
0.841666667
0.841468254
0.84147101
0.841470985
0.841470985
0.841470985
0.841470985
0.15853
0.008138
0.0002
2.73e06
2.5e08
1.6e10
7.6e13
2.78e15
0
As you can see, the difference is dramatic. In fact, the error curve for the
sine function in Figure 6.3 is rather dull; the error is not even visible after
the third term because of the all-important factorial in the denominator. By
contrast, the error behavior of the tangent in Figure 6.2 displays the classic
1/n behavior, as it should, because the denominators of Equation [6.2] are
proportional to n.
133
2n + 1
[6.4]
x
x
- ----------------------------v n = -------------2n + 1 2 ( n + 1 ) + 1
2n + 1
2n + 3
x
x
- -------------= -------------2n + 1 2n + 3
2n + 1
= (x
1 - -------------x ) -------------2n + 1 2n + 3
2n + 1
2n + 3 x ( 2n + 1 )
) ----------------------------------------------- .
( 2n + 1 ) ( 2n + 3 )
Except for the smallest values of n, you can ignore the additive terms 1 and 3
and write
[6.5]
1
v n -------2- .
2n
So the error is not really proportional to 1/n, but to 1/n2, which is still bad.
How bad? Suppose you require eight significant digits. That means you will
need
1 - = 1.0 10 8 ,
------2n 2
134
2n = 1 10
1
2
8
n = --2- 10 ,
which leads to
[6.6]
n = 7, 071.
If you agree that calculating more than 7,000 terms is not practical, you will
see that something radically different must be done to get a practical algorithm.
If the series for the arctangent converges so slowly, why use it? Maybe
youd be better off using the arcsine or arccosine series, after all. Fortunately, and perhaps surprisingly, you can convert this series to a different
form that converges very fast, which is what this chapter is all about. Ill use
some of the same tricks that I used in previous chapters limiting the range
of the input argument and using identities to compute the values for other
arguments. Most importantly, however, Ill use a trick that you havent seen
before and one that you would be well advised to add to your toolbox. The
trick is to simply recognize that the infinite series is not the only way to represent a function. If the series doesnt do the job, its time to look for
another, completely different representation, which often is dramatically
better than any tweaked form of the original series.
135
is decidedly nonlinear.
At that point in my career I had heard of a functional form called continued fractions, but I didnt know much about them and didnt have time
to learn. But this tidbit of knowledge inspired me to try a solution in the
form of a ratio of polynomials:
[6.7]
P(x)
(x) = ----------- ,
Q(x)
[6.8]
x ( 1 + ax )
(x) = -----------------------------------2
2
1 + x ( b + cx )
I dont know about you, but where I come from, five terms is better than
7,000 times. Such is the power of rational polynomials.
Although the form I chose to fit the function was purely empirical, it was
based on a certain amount of logic. First, I knew that for small x, the formula had to reduce to
tan1 ( x ) = x ,
so I knew that the numerator must have a leading value of x. For the same
reason, the rest of the function must reduce to 1.0 at x = 0. The higher powers of x let me tweak the detailed shape of the curve.
But what values should I use for the three coefficients a, b, and c?
Because time was a factor, I took the cowards way out. I simply forced the
function to be exact at four points: 0, 45, and two angles in between. This
gave me three linear equations to solve for the coefficients. I wont give you
the values, because they are not optimal, but the approach gave me a practicable formula in a short time, and the resulting implementation met all the
needs of the program. Ah, the beauty of empiricism.
136
Rigor Mortis
After all the dust had settled, I knew what I had accomplished, but I wasnt
quite sure how, or what the mathematical basis for the formula was. A year
or two later the subject came up again when I needed an arctangent function for my then-new Radio Shack TRS-80 (Level I BASIC had no trig functions). This time, because I had some time to spend, I resolved to derive the
formula with a lot more rigor and see if I could find a more general solution.
Because long constants in BASIC take up valuable character space in the
source program (which was not tokenized), I was also hoping to find some
equivalent to Equation [6.2], in which the coefficients are all nice, short
integers. The derivation that led me to a general solution represents the
main thrust of this chapter, as well as an odyssey into the world of continued fractions. Follow along with me and youll see it unfold.
Ill begin at the beginning, with Equation [6.2], which is reproduced
here.
3
11
1
x - + ---x - ---x - + ---x - -----x -+
tan x = x ---3 5 7 9 11
Its easy enough to see that x is present in every term, so a good beginning
would be to factor it out.
1
tan ( x ) = x(something).
Youll also note that the something must reduce to 1 for small x. Were on
the right track. The trick was to figure out what that something was. Just
replacing it by the factored version of Equation [6.2] is no good Id still
have slow convergence. Remembering my rule about nonlinearity, not to
mention my past success with a rational fraction, I decided to try the form
[6.9]
1
x
tan x = ------------------,
1 + P(x)
[6.10]
x 1 + P(x) = -------------1
tan x
or
x - 1.
P(x) = -------------1
tan x
137
[6.11]
10
12
x 4x 44x
438x
10, 196x
1, 079, 068x
P(x) = ----- -------- + ----------- ------------------ + ------------------------- ---------------------------------- + .
3 45
945 14, 175 467, 775
638, 512, 875
Yes, the coefficients look messy, and no, I didnt have a clue how to write
the general term. However, I was greatly encouraged by the form of the
series. First, I saw that it only contained even powers always a good
omen. Second, I saw that the coefficient of the first term was the simple fraction, 1/3. Finally, I noted that the terms have alternating signs, which aids
convergence.
Thus encouraged, I decided to see if I could find a form that would simplify the remaining coefficients in the series. Therefore, I continued with the
form
2
[6.12]
x
P(x) = ---------------------------- .
3 ( 1 + Q(x) )
[6.13]
4x 12x
93x
7, 516x
Q(x) = -------- ----------- + --------------- --------------------- +
15 175 2, 635 336, 875
[6.14]
4x
Q(x) = ------------------------------15 ( 1 + R(x) )
[6.15]
9x 16x
624x
R(x) = -------- ----------- + ------------------
35 245 18, 865
138
1
x
tan x = ----------------------------------------------------------------------------------------------------- .
2
x
1 + -------------------------------------------------------------------------------------------2
4x
-
1 + -----------------------------------------------------------------------2
3
9x
15 1 + --------------------------------------------------
35 ( 1 + something )
By canceling each leading term with the denominator of the next term, I got
an even simpler form.
[6.17]
1
x
tan x = -----------------------------------------------------------------2
x
1 + --------------------------------------------------------2
4x
3 + ----------------------------------------------2
9x
5 + -------------------------------------7 ( something )
Although my algebra skills had just about run out of gas, I saw that I
already knew enough to see the pattern. The leading constants in each
denominator comprise the class of successive odd integers: 1, 3, 5, 7, .
Similarly, the numerators are the squares of the successive integers: 1, 2, 3,
. This gave me just enough information to guess that the pattern continues in the same way.
[6.18]
1
x
tan x = ----------------------------------------------------------------2
x
1 + -------------------------------------------------------2
4x
3 + ---------------------------------------------2
9x
5 + -----------------------------------2
16x 7 + --------------------------2
25x 9 + ----------------11 +
139
140
Table 6.3
Figure 6.4
Comparison of accuracy.
Terms
Power Series
Continued Fraction
1
2
3
4
5
6
7
8
9
10
1.0
0.666666667
0.866666667
0.723809523
0.834920634
0.744011544
0.820934621
0.754267954
0.813091483
0.760459904
1.0
0.750000000
0.791666667
0.784313725
0.785585585
0.785365853
0.785403726
0.785397206
0.785398238
0.785398135
Is There a Rule?
Since doing the conversion described above, from series to continued fraction, Ive done a fair number of others. It might interest you to know that
you neednt confine yourself to series; you can apply the same technique to
141
Table 6.4
Then:
2 =
1, 2, 2, 2, 2, 2, 2,
3 =
1, 2, 1, 2, 1, 2, 1, 2,
5 =
2, 4, 4, 4, 4, 4, 4,
= 2, 1, 1, 1, 4, 1, 1, 1, 4, 1, 1, 1, 4,
= 1, 1, 1, 1, 1, 1, 1, 1, 1, (golden ratio)
= 2, 1, 2, 1, 1, 4, 1, 1, 6, 1, 1, 8, 1, 1, 10, 1, 1, 12,
= 3, 7, 15, 1, 292, 1, 1, 1, 2, 1, 1, 14, 2, 1, 1, 2, 2, 1, 4,
As you can see, all except the pesky turn out to have simple forms
when expressed as continued fractions. Note that this doesnt make the
numbers any less irrational: because both the decimal and continued fraction forms go to infinity, neither can be converted to a ratio of two integers.
However, in the continued fraction form, the coefficients do have a pattern,
which is not true for decimal digits. Only has no repetitive form in this
representation. Later, however, youll see that you can give it a regular form
also.
The continued fraction form delivers one great advantage over the decimal form of the number. By truncating, you can get successively better rational approximations to the number. The approximations for the square root
of two are shown below as an example. Each successive ratio represents a
better approximation to the actual value than the previous one. Contrast
this to the process of finding rational approximations by trial and error. In
that case, increasing the denominator by no means guarantees that the next
approximation is an improvement over the previous one.
142
3 7 17 41 99
2 1, ---, ---, ------, ------, -----2 5 12 29 70
For the record, the last ratio happens to be that used by ancient mathematicians, who wrote it in the form
1 10 14
2 --- ------ + ------ .
2 7 10
1. Since this chapter was written, the author found two methods, due to Wallis and Euler,
that give general ways to convert a power series to its equivalent continued fraction form. See:
Battin, Richard H., An Introduction to the Mathematics and Methods of Astrodynamics,
AIAA Education Series, American Institute of Aeronautics and Astronautics, 1999, pp.4468.
143
[6.19]
1
x
tan x = --------------2
x1 + ---3
3x
= -------------2- .
3+x
The math to rationalize the continued fraction is not as tedious as that for
the synthetic division, but its tedious enough. Fortunately, programs like
Maple, Mathcad, or Mathematica handle such problems with ease. In Table
6.5 Ive given the equivalent rational fractions for all orders through Order
11. For the record, the formula I used in my original empirical solution was
equivalent to that shown as Order 7. Using Mathematica, Ive worked out
the formulas through Order 21, but its highly unlikely that youll ever need
them.
Table 6.5
Order 1
3x ----------3+y
Order 3
x ( 15 + 4y )
-------------------------15 + 9y
Order 5
x ( 105 + 55y )
--------------------------------------2105 + 90y + 9y
Order 7
2
Order 9
2
Order 11
144
Table 6.6
Fraction
Error
1
3
5
7
9
11
1/1
3/4
19/24
160/204
1,744/2,220
2,576/3,280
0.214601836
0.035398163
0.006268503
0.001084437
0.000187422
0.000032309
145
lower values of x. To put it another way, the error curve becomes flatter,
with a sharper knee near x = 1.
This characteristic of the error curve brings us back to the guiding principle learned in previous chapters: to get even more accuracy, you dont need
to improve the formula, you simply need to limit the range of the input
argument.
Youve already seen two implicit reductions that are very powerful,
although I didnt make much of them at the time. The tangent of an angle
ranges all the way from to +. However, note that the error curves Ive
drawn and the calculations Ive made are always in the range 0 to 1. Even
more importantly, the power series and the continued fraction derived from
it are not even valid for values of x outside the range 1 to 1.
Figure 6.5
I can reduce the range in two steps. First, the tangent function is an odd
function odd in the mathematical, not the usual, sense.
[6.20]
tan ( x ) = tan ( x )
Using this identity reduces the range to only positive values. Thats a shame
because the formula is equally valid (and equally accurate) for negative values. However, eliminating negative values leaves room to make further
146
Listing 6.1
Arctangent wrapper.
/* First wrapper for arctangent
* Assures positive argument
*/
double arctan(double x){
if(x < 0)
return -arctan1(-x);
else
return arctan1(x);
}
Because the power series in Equation [6.2] is only valid for input values
of x 1, I cant use the series, or the continued fraction derived from it, as it
stands except when it produces angles in the range 0 to 45. So how do I
deal with larger input values?
This problem also turns out to be trivially easy. A second trig identity
gives
[6.21]
-.
tan = cot --- = ------------------------
2
tan ---
2
[6.22]
1
1
tan x = tan --- .
x
To solve for the arctangent of arguments greater than one, simply take the
reciprocal of the input, find its arctangent, then subtract the result from .
In this way, those seemingly impossible cases where x approaches infinity
become trivial. In fact, the closer I get to infinity, the faster the convergence.
If I could represent a true infinity in the machine, its reciprocal would be
zero and so would the output angle of the arctangent function.
Listing 6.2 is a second wrapper that deals with input values larger than
1.0. Between this wrapper and that in Listing 6.1, I can get an argument
147
Listing 6.2
tan a + tan b
tan ( a + b ) = --------------------------------1 tan a tan b
However, dont forget that Im seeking the arctangent, not the tangent. Its
sometimes difficult to remember what the input is and what the result is.
Some changes in notation will help me keep my goal more easily in view.
Assume that the input value x is
[6.24]
x = tan ( a + b ) .
Also, let
[6.25]
[6.26]
tan a = z
and
tan b = k ,
[6.27]
a = tan ( z )
and
[6.28]
b = tan ( k ) .
148
z + kx = ------------1 zk .
x kz = 1-------------+ kx .
tan x = a + b .
0 k- = k
z(0) = -------------1 + 0k
[6.33]
1 kz(1) = ----------1+k
The first value is clearly negative, with magnitude less than one, if k is
itself less than one. The second value is clearly positive if k is less than one,
and will also be less than one. Thus, by applying the offset, Ive gotten back
the negative side of my input argument. In doing so, Ive reduced the maximum excursion to something less than one. Figure 6.6 shows the relationship between x and z for the case k = 1/2. Note that the curve passes through
zero when x = k; this must always be true, as is clear from Equation [6.30].
Figure 6.6
149
xz relationship.
Note that I got this reduction without having to split the range up into
regions and use different values of k (and therefore b) for each region. I still
have only one region; I always apply the offset. Perhaps even more importantly, the same method opens the possibility of further range reductions,
either by applying this one recursively or by breaking the input range into
regions with a different k and b for each region. Either approach will let me
reduce the range to an arbitrarily small region.
150
Equal Inputs
Because the range of the input variable is 0 to 1, it seems reasonable to split
the difference and let k = 1/2. For this value, the arctangent is
[6.34]
21
1
z(1) = ------------ = --2+1
3
The error curve for this value of k and an 11th-order approximation are
shown in Figure 6.7.
Figure 6.7
One look at this error curve gives graphic evidence of both the good and
bad sides of the equal-value approach. On the one hand, Ive reduced the
maximum error, which now occurs at x = 0, from 0.000035 to barely 2
108 a dramatic reduction by more than a factor of 1,500. On the other
hand, the errors at the two extremes are horribly unbalanced; the error at x
= 1 is not even visible: its 130 times smaller than at x = 0. This is a sure
sign that I have chosen a value of k that is far from optimal. Intuition and
151
experience tell me that I am most likely to get the best fit, that is, the lowest
maximum error, when the errors at the two extremes are equal in magnitude. This is the minimax principle, and it is a powerful one.
Folding Paper
You can gain wonderful insight into the nature of the range reductions using
the following thought experiment. Imagine that you cut a round circle out
of paper. This circle represents the full range of possible angles, from 0 to
360. If it helps you to visualize the situation, imagine drawing horizontal
and vertical lines on the circle, dividing it into four quadrants.
Although the angle can clearly fall into any of the four quadrants, the
arctangent algorithm sees only two the first and fourth. Thats because
the values of the tangent function for angles in the second quadrant are
identical to those in the fourth, just as the values in the first quadrant are
identical to those in the third. Because the arctangent function has no way
to distinguish between the two pairs of quadrants, it traditionally returns
values only in the first and fourth quadrant. To mirror this behavior, fold
your circle about the vertical diameter into a semicircle, as in Figure 6.8-1.
Figure 6.8
152
Balanced Range
Now you can see that for optimal error performance you should use an offset that balances the output angle, not the input argument. Accordingly, let
k be chosen so that
[6.36]
k = tan ( 22.5 ) .
Its easy enough to calculate this value on a pocket calculator, but youll
gain a little more insight into the situation by applying Equation [6.23],
with a = b = /8.
tan a + tan b
tan ( a + b ) = ------------------------------1 tan a tan b
153
[6.38]
1 q = 2q
2
q + 2q 1 = 0.
This is a quadratic equation, so it has two roots. Applying the quadratic formula gives
q1 =
21
q2 = 2 1 .
Because the second root gives a negative value, Ill accept the positive one and
write
[6.39]
q = tan --- =
8
21.
In this case,
[6.40]
z(0) = ( 2 1 ) .
154
21
or
[6.41]
z(1) = k .
Using equal output angles does indeed balance the range. The value of z,
which is the argument for the continued fraction, now ranges between k
and k, which will always be less than one. Because the error is a function of
z, the errors at the two endpoints will also be equal, and the minimax condition will be achieved.
[6.42]
An Alternative Approach
I should pause here to mention that I could have approached this solution
from the opposite end. I arrived at the balanced-angle solution by looking at
the paper-folding example of Figure 6.8, and I rightly reasoned that equal
arguments, z, would result in equal magnitudes for the errors at the two
extremes. Suppose, however, I hadnt been able to figure this out. I could
still have arrived at the same result by requiring the minimax condition,
Equation [6.42], and working backward to determine k. The errors at the
extremes will be equal in magnitude if
[6.43]
z(0) = z(1) .
Proceeding from there and using the definition for z given in Equation
[6.30], I get
1k
k = -----------1+k
155
or
k(1 + k) = 1 k
[6.44]
k + 2k 1 = 0 .
k =
2 1,
which is the same result I got by assuming k was the tangent of 22.5.
Either way, this value of k is optimal for the range x = 0 to 1. A function
to reduce the range using these values is shown in Listing 6.3. The optimized error curve is shown in Figure 6.9. (Note the call to the library function _atan() in Listing 6.3. Im assuming that this function is provided by
the programmer using one of the continued fraction forms. Remember, the
whole purpose of this exercise is to provide the library function. If your
compiler already has a satisfactory one, Im wasting your time.)
Listing 6.3
Reduction by rotation.
/* Third wrapper for arctangent
* Reduces argument via rotation
*/
double arctan3(double x){
static const double b = pi/8;
static const double k = root_2 - 1;
return(b + _atan((x - k)/(1 + k*x)));
}
Ive now reduced the maximum error another order of magnitude compared to Figure 6.7 and a full factor of 15,000 less than in Figure 6.5.
Clearly, reducing the range by including an offset makes a profound difference in error behavior especially if the offset is optimized and that, in
turn, allows you to use a lower order approximation. The degree of
improvement can be seen clearly in Table 6.7, where Ive given the maximum error for each order of approximation.
156
Figure 6.9
Its worth noting again that I achieved this range reduction without having to test the argument. I simply applied Equation [6.31] in all cases, producing an effective rotation of the octant by 22.5.
However, I could easily perform further reductions by repeating the process, using splits every 11.25. The last column of Table 6.7 shows the errors
if I do this. Just as in the paper-folding exercise, you can continue this process as long as you like.
Table 6.7
Full Range
One Reduction
Dual
Reduction
1
3
5
7
9
11
0.214602
0.035398
0.006269
0.001084
0.000187
3.23e05
0.021514
0.000893
3.63e05
1.46e06
5.81e08
2.31e09
0.002563
2.64e05
2.63e07
2.6e09
2.54e11
2.48e13
157
The two-reduction case involves only a simple range test. The code to
implement this reduction is shown in Listing 6.4. The errors for this twostage reduction are shown in the third column of Table 6.7.
Listing 6.4
As you can see from the table, the reduction in errors attributable to
using the optimal offset is quite dramatic. Where before, even Order 11 was
inadequate over the full range, I can now get single-precision accuracy with
Order 7 using one reduction and with Order 5 using two. With two reductions, I can get accuracy much better than one part in 108, using only Order
7 the same order I used in my first attempts. This kind of accuracy is a far
cry from that given by the series approximation I started with. To grasp the
full effect of the improvement, take another look at Table 6.5 and note the
extreme simplicity of the equations for Orders 5 and 7.
158
159
0.125. At this magnitude, you can expect the error to be around 1.0e8,
using the fifth-order formula, or 4.0e11, using seventh order. Figure 6.10
shows the error behavior for the equal-value fifth-order case. In this figure,
Ive left off the absolute value computation so you can see the discontinuities in the errors from segment to segment.
Listing 6.6 shows the code for an equal-angle approach, also using four
segments.
Listing 6.5
=
=
=
=
0.124354994;
0.358770670;
0.558599315;
0.718829999;
k*x)));
160
Listing 6.6
const
const
const
const
const
const
k, b,
double
double
double
double
double
double
z;
b_mid = pi/8;
b_hi = 3*pi/16;
b_lo = pi/16;
k_mid = tan(b_mid);
k_hi = tan(b_hi);
k_lo = tan(b_lo);
For purposes of comparison, Ive also shown the error behavior for the
equal-angle approach in Figure 6.10. Comparison of the two curves thoroughly
161
illustrates the superior accuracy of the equal-angle case. However, you can
expect the search process needed to locate the correct segment to be more time
consuming than for the equal-value case. More importantly, the equal-input
approach can be extended much more easily to larger numbers of segments.
The bottom line is that optimality of the errors becomes less and less important
if theyre well within bounds.
Which approach is best? As I said at the outset, the answer is not obvious, which is why Im giving you both methods. However, in the remainder
of this chapter, Ill assume the equal-angle method because of its superior
error behavior.
162
tan(x) x.
Table 6.8
163
Segments
1
0.2146
0.0354
6.27e3
1.08e3
1.87e4
3.23e5
5.56e6
9.57e7
1
3
5
7
9
11
13
15
9
1
3
5
7
9
2.22e4
4.51e7
8.85e10
1.71e12
3.32e15
3
6.15e3
1.13e4
2.01e6
3.54e8
6.19e10
1.08e11
1.88e13
3.28e15
5
1.30e3
8.60e6
5.48e8
3.45e10
2.16e12
1.34e14
7
4.73e4
1.59e6
5.16e9
1.65e11
5.26e14
11
13
15
1.22e4
1.65e7
2.17e10
2.81e13
7.36e5
7.17e8
6.73e11
6.24e14
4.79e5
3.50e8
2.47e11
1.72e14
Getting Crude
Now that youve seen the theoretically correct approximations, perhaps
its time to ask what the least complicated algorithm is that you can find.
The obvious first choice must be the first-order approximation
tan1(x) = x,
but this is crude indeed. The main problem is that the fit gets worse and
worse as x gets larger, although the formula is exact for values of x near
zero. But all is not lost. With a little tweaking, you can still use the idea of a
linear equation but trade some of the error within the range for better accuracy near the end. Accordingly, assume that
[6.47]
tan1(x) = ax.
You now have a coefficient that you can play with to improve the fit.
(You might be wondering why I didnt generalize further and use an offset
164
165
Table 6.9 shows the optimized coefficient and the maximum error for
four choices of segmentation.
Table 6.9
Maximum Error
full (0 to 1)
1
3
5
7
0.83327886
0.96073345
0.98417502
0.99401079
0.99690021
0.0478807008
5.2497432649e3
1.3335152673e3
3.0956013149e4
1.1516239849e4
These results are nothing short of remarkable. With only three segments,
which are really only two (0 to 15 and 15 to 45), and with only a simple
first-order approximation, I got an error less than 0.8. This kind of error is
too great for high-precision work, but for many real-time embedded applications, the accuracy of this simple algorithm is more than sufficient. With
seven half-segments, you achieve an accuracy three times better than the
7th-order continued fraction was able to provide over the full range, 0 to 1.
If youre looking for a quick-and-dirty algorithm that requires very little
arithmetic, look no further. This is it.
166
167
The solution is also the same: minimax. To optimize the error, the error
curve must cross zero a certain number of times (the more, the better).
Youll recall I got my first empirical formula by forcing zeros at the endpoint
of x = 1 (output = 45) plus two points in between. To see the way the
method works, Ill revisit it precisely as I did it 22 years ago. Ill begin by
repeating Equation [6.8],
2
x(1 + ax )
(x) = ----------------------------------2
2 ,
1 + x (b + cx )
then define the three values of x for which the formula is exact.
2
[6.48]
x i (1 + ax i )
1
tan ( x i ) = ----------------------------------2
21 + x i (b + cx i )
( i = 1, 2, 3 )
Although it may not look like it, this equation represents a linear equation
in the three unknowns a, b, and c (remember, here the xs are not the variables were using them as constant input variables, and seeking to find
the corresponding values of the constants a, b, and c). To see this more
clearly, multiply the denominator to get
1
tan ( x i ) [ 1 + x i (b + cx i ) ] = x i (1 + ax i ) .
168
I now have a linear algebra problem, which I can cast into matrix form:
[6.50]
x 0 tan ( x 0 )
x 1 tan ( x 1 )
x 2 tan ( x 2 )
x0
x 0 tan ( x 0 )
A = x3
1
x 1 tan ( x 1 )
x 2 tan ( x 2 )
x2
and
1
x 1 tan ( x 1 )
[6.51]
U = x tan 1( x ) .
2
2
1
x 3 tan ( x 3 )
[6.52]
a
k = b = A1U
c
x 2 = tan ( 30 ) = 0.57735
x 3 = tan ( 45 ) = 1.0
a = 0.4557085
b = 0.78896303
c = 0.06450259
169
The resulting error curve is shown in Figure 6.14. As you can see, the error
function does indeed cross zero at the points specified. The error is zero at x
= 0, x = 1, and the two points in between. Between these zeros are hills and
valleys as expected. The amplitudes of these hills and valleys, however, are
necessarily affected by the need to pass through the zeros.
You wont fully grasp the significance of this error curve until you compare the maximum error with that of Figure 6.13. In that graph, the maximum error was about 0.001. The new error curve never exceeds a value of
about 4.2 105 an improvement by a whopping factor of 24. This error
is equivalent to an angle of 0.002, which was more than adequate for my
needs at the time. Figure 6.15 shows the two error curves together for easy
comparison. Shown on the same scale, the lump in Figure 6.14 doesnt loom
nearly so large.
170
But this is only the beginning. One look at Figure 6.14 should also tell
you that my choices for x1, x2, and x3 were far from optimum. The strategy
inherent in the minimax approach is to seek coefficients such that the height
of the highest peak (positive or negative) was minimized. A little reflection
should convince you that this is best accomplished when all the peaks have
the same magnitude. If that isnt true, if one peak is higher than the others,
you would be able to reduce it by shifting the values of x around a bit. This
shifting would adversely affect the other peaks in turn. Therefore, youre
going to get the smallest worst case error when all the peaks are the same
magnitude. This optimum situation, which represents the minimax solution,
occurs when all the peaks have equal amplitudes and alternating signs.
Ive done just that kind of tinkering for the seventh-order function given
in Equation [6.48]. The optimum values are as follows.
x 1 = 0.542
[6.55]
x 2 = 0.827
x 3 = 1.0
[6.56]
a = 0.42838816
b = 0.76119567
c = 0.05748461
171
The new error curve is shown in Figure 6.16. You can see how the minimax
process balances the heights of all the peaks. The maximum error is now
down to less than 8 106, which is equivalent to 0.00046, or only 1.65
seconds of arc. Thats a small angle in anybodys book (its about the apparent diameter of a dime, one mile away), and more than ample for almost
any embedded application short of the Hubble Space Telescope. Through
minimaxing, Ive managed to increase the accuracy of the seventh-order formula by a whopping 125 times.
You may be wondering how I found the magic values of the coefficients.
Twenty years ago, when I was seeking a true minimax solution for Equation
[6.8], I found the coefficients by brute force. I programmed my old Tandy
TRS-80 to search all combinations of a, b, and c until the maximum error
was minimized. Time was not an issue here; I needed to compute the numbers only once, and since the computer didnt have much else to do, I was
content to let it grind all day, all night, or even all week, if necessary. I did
give the program a few smarts, in that it started with a fairly course step size
and reduced it as it got near the optimal solution, but mostly it performed
an exhaustive search. More sophisticated computer programs use generalpurpose optimization algorithms to find the solutions in much less time.
However, the approach you just saw, in which you specify the values of x
at the zero crossings and use linear algebra to compute the coefficients, is a
more indirect, but far more effective, way of optimizing the coefficients. The
172
Mathcad
For some years now, Ive strongly recommended Mathcad, the math
analysis program from Mathsoft, Inc. My usual comment is, If you
dont have Mathcad now, get it now!
Considering that background, you can imagine my embarrassment, not to mention disgust, when I upgraded to Mathcad v7.0, only
to find that it was seriously broken. Im not talking about minor
inconveniences here; Im talking major errors.
I have discussed this problem with the folks at Mathsoft, who have
always been very nice and helpful, and they explained that the Mathcad user interface had been heavily modified based on feedback from
users. Apparently, this also required changes in the interface between
Mathcad and Maple, which Mathcad uses as its symbolic engine.
The Mathsoft support person told me that they were aware of
some of the problems Id found, and she supplied patches to fix them.
Other problems, she tried unsuccessfully to convince me, were not
bugs, but features. I must admit that the patched version is much
more reliable and doesnt give me OS-crashing errors as before, but I
still dont like the new user interface much, and those features are still
bugs. More recently, I received Mathcad 8.0 which was worse yet and
required three more patches to be usable. Mathcad 2000 is more of
the same.
Someone pointed out that the problem with listening to complaints
from users, in regard to user interfaces, is that the people youre most
likely to hear from are the beginners who have just started using the
173
program and cant figure out how to do things. Those of us who have
been using the program for a while have learned sometimes easily,
sometimes not how to deal with the interface thats there. We dont
tend to call in to complain.
Although its nice to have a program thats easy for new users to
learn, its even more important to have one that power users can
use effectively. Sometimes, listening only to the complaints of new
users can lead one down the garden path.
In my personal opinion, Mathcad version 7.0 and beyond, patched
or not, is a distinct step downward from version 6.0, which was itself
a step downward from version 5.0. Everything takes longer to do and
is less reliable than in the older versions.
Because Ive recommended Mathcad so strongly to my readers for
a few years now, this puts me in an awkward position. I still like the
program, but in all conscience, I cant recommend version 7.0 or later,
patches or no patches. My best advice to you is, if you have version
6.0 or earlier, do not upgrade! If you havent bought a copy yet, try to
find a copy of Mathcad v5.0. (see www.recycledsoftware.com)
x1 = 0.533
x2 = 0.808
x3 = 0.978
174
[6.58]
a = 0.43145287
b = 0.7643089
c = 0.05828781
This step doesnt reduce the error much, but its now about 6 106, a
reduction of 25 percent. Is a reduction of error by 25 percent worth accepting a discontinuity in the error behavior at the boundaries between regions?
My gut feel is that its not. Id rather be sure that the error remains continuous at the boundaries. On the other hand, if the error is small enough, perhaps nobody cares what its value is. You are free to decide for yourself.
Surely, Im finished with the example formula now, right? No, theres still
more performance to squeeze out of it. Remember when I optimized the
error in the first-order case by introducing a scale factor? I have not yet
done that for this seventh-order formula. This is reflected by the zero slope
of the error curve when x = 0. Adding the extra scale factor frees up this
constraint, which affords a better chance to reduce error peaks. The new
formula is
2
[6.59]
x ( a + bx )
7(x) = ------------------------------2
4- .
1 + cx + d x
175
I now have four coefficients to solve, so Ill need four equations, and Ill
define four values of x, instead of three, for zero crossings. I wont bore you
with the details here, but the technique is almost identical to that used previously; the matrix is simply one order larger. The optimum results are
[6.60]
a
b
c
d
=
=
=
=
0.99995354
0.42283387
0.75531298
0.05624676.
=
=
=
=
1.0
0.52380952
0.85714286
0.08571429.
You can see that the minimax process changes the coefficients significantly,
at least for this extreme range of 0 to 1. The smaller the range, the closer the
coefficients come to their theoretical (i.e., nonminimaxed) values.
The error curve for this last optimization is shown in Figure 6.18. The
maximum error is now down to 4.6 106, which represents a reduction
from the original error curve by a factor of more than 250. Now you can
see how my empirical seventh-order approximation served my needs so
well.
176
A Look Backward
Table 6.10
Order
1
3
5
7
9
11
177
11
0.048
2.27e3
9.75e5
4.58e6
2.21e7
7.71e9
1.52e3
7.08e6
3.11e8
1.54e10
3.25e4
5.11e7
8.19e10
3.42e12
1.18e4
1.00e7
3.26e11
5.55e5
2.97e8
< 2.38e12
3.04e5
1.04e8
Although its taken a lot of time, Ive lovingly and laboriously performed
the minimax optimizations on all the practical combinations of segment
sizes and orders of approximation. Space does not permit giving the coefficients, but Table 6.10 gives the resulting maximum errors for odd segment
numbers (as in Table 6.8). Table 6.11 gives similar results for even numbers
of segments (assuming zero error at the boundaries). Compare these three
tables, and youll see how truly far the process of error minimization has
come. Pick one of the combinations that fits your needs and enjoy the richness of its performance.
Table 6.11
Order
1
3
5
7
9
10
12
5.24e3
5.13e5
5.56e7
5.31e9
1.079e10
6.79e4
1.80e6
4.76e9
1.85e11
1.87e4
2.19e7
2.64e10
9.33e12
7.94e5
5.11e8
3.15e11
4.021e5
1.774e8
7.55e12
2.327e5
6.58e9
1.987e12
A Look Backward
At this point, its hard to remember how difficult this problem seemed to be
in the beginning. Therefore, it might be worthwhile to review the process
from start to finish.
178
A Look Backward
179
have more than enough insight to roll your own algorithm to suit your specific needs. Use Tables 6.10 and 6.11 to guide your decisions. Have fun.
Listing 6.7
for segmentation
double b = pi/6;
double k = tan(b);
double b0 = pi/12;
double k0 = tan(b0);
180
Listing 6.7
7
Chapter 7
181
182
A Minireview of Logarithms
If youre already comfortable with the concept of logs and antilogs, you can
skip this section. If, like most folks, you havent used logarithms since high
school or college, this minireview might help. Ill begin at the beginning,
which is the integral powers of 10 that are used in scientific notation.
Consider the square of 10:
102 = 10*10 = 100.
Similarly,
103 = 10*(102) = 1,000,
104 = 10*(103) = 10,000, and so on.
If you examine the pattern of these numbers, youll see that the number of
zeros in each number is exactly the same as the exponent in the power-of-10
notation. Thus, 10 to any integral power n is just a one followed by n zeros.
Although the idea of scientific notation isnt really the topic of my current focus, I will be using it later, and Im close enough to the idea here to
A Minireview of Logarithms
183
give it a brief mention. Scientists often have to deal with numbers that are
very large, like the number of stars in the universe, or very small, like the
mass of an electron. Writing such large or small numbers gets to be tedious
very fast, so scientists, being a lazy lot, have devised a notation that lets
them express such numbers concisely. Simply take one power of 10, which
gets the size of the number in the ballpark, and multiply it by a small number that defines the exact value. Thus, Avogadros number (giving the number of molecules in a mole of gas) is
6.023 1023,
which is equivalent to
602,300,000,000,000,000,000,000.
The first part of the number, 6.023, defines the first digits of the number; the
power of 10 tells how many digits lie to the left of the decimal point. For the
record, the number of stars in the universe is about 1022, and the mass of an
electron is 9.1 1031 kg.
Suppose I have two numbers raised to integral powers,
x = 104 = 10,000
y = 103 = 1,000,
and I want to multiply them. Doing it the hard way, I find that
xy = 10,000,000.
Stated in the short form, this number is 107. But the exponent seven is also
the sum of four and three, which are the exponents of x and y. Although a
single example hardly proves anything, it certainly suggests that multiplying
powers of 10 is equivalent to adding their exponents. Try it again with as
many examples as you like, and youll find that the rule always works.
To multiply powers of 10, add their exponents.
If you want to divide the same two numbers, you get
x
, 000
-- = 10
------------------ = 10.
y
1, 000
184
On the other hand, you can think of this division as the product of two
numbers: x and the reciprocal of y.
, 0001 -
--xy = 10
----------------= 10, 000 ------------- 1, 000
1, 000
You already know that the first number is 104. You also know that you
can multiply any two powers of 10 by adding their exponents. To make the
rules come out right, you are left with the inescapable conclusion that the
fraction, 1/1,000, is equal to a negative power of ten.
1 - = 10 3
-------------1, 000
From this result, you can extend the notion concerning powers of 10 to
negative powers, and thus to numbers less than one. The only thing is, you
must change, a little bit, the way the rule is written. First, the exponent of
powers of 10 tells you how many zeros to place after the 1. This rule doesnt
really work properly if you try to extend to negative powers. A better way
to state the rule is, the power of 10 tells you how many ways to move the
decimal point, left or right. Thus, to write the number 104, first write the
digit 1, followed by a decimal point:
1.0
To get the number 104, you still write the 1 but move the decimal point
four places to the left:
1.0 0.0001
In this case, the number of zeros to the left of the one is not the value of
the exponent, but one less. Thats because the original decimal point was to
the right of the one at the start. Nevertheless, if you think about the process
A Minireview of Logarithms
185
as moving a decimal point, rather than adding zeros, youll get the right
value.
You need to consider one last facet of the rules. What if you divide two
powers of 10 that are equal; for example,
x
, 000- = 1.
--x = 10
----------------10, 000
Table 7.1
Powers of 10.
n
10n
5
4
3
2
1
0
1
2
3
4
5
0.00001
0.0001
0.001
0.01
0.1
1.0
10.0
100.0
1,000.0
10,000.0
100,000.0
186
10
log x
= x.
A graph of the function is shown in Figure 7.1. As you can see, log(1) = 0,
and log(10) = 1, as advertised.
Figure 7.1
Take a Number
At this point, you might wonder how I obtained this curve. As it turns out,
there is a power series for it, which youll see in a moment, as well as much
better representations, which youll also see. Like most power series representations, the derivation of this one comes via calculus. However, if you
know how to raise 10, or any other number, to a nonintegral power, you
can conceivably find the log function even without such a power series by
using simple iterative methods.
To find the logarithm of, say, three, pick a number, any number. Ill guess
1.0. 101 is 10, and thats way too high, so Ill cut my guess in half to 0.5.
Now raise 10 to the power 0.5. Whats that? You dont know how to raise a
number to a fractional power? Sure you do, in this case, at least. Any num-
A Minireview of Logarithms
187
ber to the power 0.5 is the same as the square root of that number. The
square root of 10 is 3.16, so Im not too far off. Ill try 0.4. Ten to the power
0.4 is 2.51, so thats too low. I can keep trying numbers between 0.4 and 0.5
until I find 10n = 3.
But how did I know that 100.4 = 2.51? I cheated: I used my calculator. But
my calculator used the logarithm to compute the value. I seem to be stuck in
a twisty little maze of circular logic. To find the logarithm of a number, I need
to raise 10 to a nonintegral power. But to do that, I seem to need the logarithm function that I dont know yet.
So how do I calculate the value of 10 to an arbitrary nonintegral power?
One solution will always get me arbitrarily close to the correct answer.
Because my guess of 0.5 was too high, Ill try half of that, 0.25. Im now
seeking 101/4, which is simply the square root of the square root of 10. Its
value is 1.77, which is way too low, but at least it gives me a starting point
for the next guess.
By taking the average of the too-high and too-low values, I can get
another value thats hopefully closer. If I continue this process, I will always
get an exponent (logarithm) of the form
[7.2]
P- .
---q
2
I know I can always raise 10 to this power, by a combination of multiplications and square roots. For example, the guess for the next step is
[7.3]
1 1 1
3
--- --- + --- = --- .
2 2 4
8
3
--8
1
--3 8
10 = ( 10 ) =
1000
= 2.37137
188
Table 7.2
10x
1
2
3
4
5
6
7
8
9
10
11
1
0.5
0.25
0.375
0.4375
0.46875
0.484375
0.4765625
0.48046875
0.478515625
0.477539063
10.000000
3.162277660
1.778279410
2.371373706
2.738419634
2.942727176
3.050527890
2.996142741
3.023213025
3.009647448
3.002887503
The denominator is 210, so you must take a square root 10 times. I must
admit that I dont particularly relish the idea of taking the square root, 10
times, of a number that starts out to be one followed by 489 zeros. Fortunately, I dont have to. Remember, you form each guess by taking the average of the two previous numbers.
[7.5]
1
x = ---(y + z)
2
Whats more, you already have the values of both y and z and the values of
10 raised to those powers. Raising 10 to the power x is equivalent to
A Minireview of Logarithms
10 = 10
[7.6]
189
1
--- ( y + z )
2
= [ 10
1
--( y + z) 2
( y + z)
10
10 10 .
Stated in words, all you need to do is to multiply the previous two powers and take the square root of the product. (This process, by the way, is
called taking the geometric mean.) For example, for the fifth guess, I averaged the second and the fourth values.
1
x 2 = --2
3
x 4 = --8
[7.7]
1 1 3
7
x 5 = --- --- + --- = -----2 2 8 16
10 2 = 3.162277660
x
10 4 = 2.3713737
x
10 5 =
3.162277660 2.3713737
= 2.7384196
Thats exactly the same value I got in Table 7.2. As you can see, you
could easily build Table 7.2 armed with nothing but a five-function calculator the fifth function being the square root. If you lived in the Renaissance period, you could do it armed with only pen, ink, and a lot of paper.
I dont propose that you try to build log tables using the kind of successive approximation process Ive used here. It was tedious enough to do it
just for a single number, and even then Im still a long way from an accurate
solution. Still, I find it comforting to know that I can break the cycle of circular logic. Even if I didnt know calculus, and therefore didnt have the
power series form, I could conceivably build a table of logarithms using
only the sweat of my brow. Its somehow nice to know that this stuff can be
done by actual humans, because ultimately, you may be the human that has
to do it. Or at least program it.
190
log(30) = 1.4771
log(300) = 2.4771
log(3,000) = 3.4771
log(30,000) = 4.4771.
where n is the integer part (the part to the left of the decimal point) and f is
the fractional part; therefore,
[7.8]
10 = 10
(n + f )
= ( 10 ) ( 10 ).
The fractional part gives a number between one and 10, and the integral
part serves to place the decimal point.
A Minireview of Logarithms
191
This function is crucial to the whole process because, like the sine function,
theres an infinite series definition that computes ex. This makes it a kind of
Rosetta stone that defines the antilogs for other bases. In computerese,
exp() always implies base e, but you can use it to get the other antilogs
because
[7.9]
10 = e
ln 10
and
2=e
ln 2
[7.10]
10 = ( e
and
[7.11]
2 = e
ln 10 x
) = e
x ln 10
x ln 2
and, conversely,
[7.12]
ln x
log x = -----------ln 10
and
[7.13]
ln x
log 2x = --------- .
ln 2
The factors ln 2 and ln 10 are constants. These relations come in handy later
on.
192
[7.14]
x
x
x
x
x
x
e = x + ----- + ----- + ----- + ----- + ----- +
2! 3! 4! 5! 6!
and
[7.15]
11
3 5 7 9 11
where
[7.16]
x 1- .
z = ----------x+1
Figure 7.2
193
Figure 7.3 shows the relationship between x and z. As x varies from one
to two, z goes from zero to 1/3. This is encouraging because you dont have
to deal with values of z near unity. Although true mathematicians would
cringe at the idea of judging the convergence of a series by looking at one
term, non-mathematicians can use a single term to at least get a feel for the
number of terms needed in the equation by requiring the last term used in
the truncated series to be less than some small value. The general form for
the nth term is
[7.17]
2n + 1
z
-,
T n = -------------2n + 1
[7.18]
z
--------------=
2n + 1
z
2n + 1
= ( 2n + 1 ).
194
z
z
2n
= 2n
1-----
= ( 2n ) 2n .
2n
= 2n
2n ln z = ln ( 2n ) + ln
[7.20]
( 2n ) + ln- .
2n = ln
----------------------------ln z
( 2n ) 18.42- .
2n = ln
--------------------------------- 1.0986
2n = 14
n = 7.
Thus, Ill need at least eight terms (n starts at zero), or powers through
to get reasonable accuracy. This actually isnt too bad the fact that z
never gets close to unity helps a lot. Still, I can do much better.
z15,
Figure 7.3
195
x versus z.
196
[7.23]
2z
ln x = --------------------------------------------------------------2
z
1 -----------------------------------------------------2
4z
3 --------------------------------------------2
9z
5 -----------------------------------2
16z 7 -------------------------2
25z 9 ----------------11 +
Rational Polynomials
As before, note that you must truncate the continued fraction at some level
even though it is theoretically infinite. Once you decide where to truncate it,
you can reduce the now-finite form to a ratio of two polynomials. Unfortunately, the differences in signs cause the rational polynomials to be different
from the arctangent equivalent. Still, they are easily computed from the continued fraction form, especially if you let a tool like Mathcad do all the hard
work for you. The truncated forms are shown in Table 7.3.
You can compute the maximum error in the formulas of Table 7.3 by setting z = 1/3, its maximum value, and comparing the fractions with the exact
value of the logarithm. The results are shown in Table 7.4.
The error behavior of these fractions is quite nice; note that the error in
the 15th-order case is five orders of magnitude better than the value I predicted for the power series. Continued fractions really are a far superior
approach. However, it will probably not surprise you to hear that you can
do much better yet.
Table 7.3
Order 1
Order 3
2z ( 15 4y )---------------------------15 9y
Order 5
Order 7
197
(y = z2)
Order 9
Order 11
Order 13
2z
( 15015 19250y + 5943y 256y )---------------------------------------------------------------------------------------------2
3
15015 24255y + 11025y 1225y
2
Table 7.4
Order 15
Absolute Error
Relative Error
1
3
5
7
9
11
13
15
0.026
8.395e4
2.549e5
7.631e7
2.271e8
6.734e10
1.993e11
5.893e13
0.038
1.211e3
3.677e5
1.101e6
3.276e8
9.715e10
2.876e11
8.502e13
x 1- .
z = ----------x+1
198
1 z- .
x = ---------1+z
Figure 7.4
199
x versus z, redux.
Familiar Ground
At this point, you should be getting a distinct feeling of dj vu. I began
with a power series that looks very much like the one for the arctangent, I
generated a continued fraction based on the series, I truncated the continued
fraction to get ratios of polynomials, and now Im talking about limiting the
range of the input argument. Thats precisely what I did with the formulas
for the arctangent, so it shouldnt surprise you to hear that most of rest of
the process of finding a practical formula involves steps very similar to those
Ive already taken for the arctangent: dividing the argument range into segments to further limit the magnitude of z and performing minimax optimization of the coefficients.
200
ln ( kx ) = ln x + ln k :
conversely,
[7.27]
ln x = ln ( kx ) ln k .
Suppose your initial argument is in the range from one to two, as used
earlier. Simply divide by a constant, k, to change the range to
[7.28]
1--- to 2--- .
k k
It should be fairly apparent that the best choice for k is one that will give
equal and opposite values of the logarithm at the two extremes; that is,
[7.29]
ln 1--- = ln 2---
k
k
or
ln ( 1 ) ln k = ln 2 ln k .
k =
2.
The range is now 0.707 to 1.414, which has a logarithmic range of 0.347
to 0.347. The corresponding value of z is 0.1716. This is just a little more
than half the original maximum z, which was 1/3, or 0.3333. At first glance,
you might expect a reduction in error at the extremes by a factor of two,
raised to the power of the approximation. In fact, you get considerably better than this. The accuracy of the various orders is shown in Table 7.5. The
magnitude of the error goes roughly as 2(n + 2), where n is the order of the
approximation.
Table 7.5
201
Full Range
Half Range
1
3
5
7
9
11
13
15
0.026
8.395e4
2.549e5
7.631e7
2.271e8
6.734e10
1.993e11
5.893e13
3.428e3
2.738e5
2.105e7
1.598e9
1.206e11
9.065e14
0
0
Can I restrict x even further? Yes. Just as in the case of the arctangent, I
can break the range into segments and basically use different values of k,
depending on the value of x. In so doing, I can divide the range into any
number N of half-segments. The first step (using the full range for z) corresponds to N = 1. The second step (using an offset k) corresponds to N = 2.
The same argument I used in the case of the arctangent also applies: odd
numbers of N are preferred over even ones, because they result in zero error
when z is near zero (x near 1.0). As with the arctangent, using odd numbers
seems to waste one half-segment, by using only the positive side of the one
nearest z = 0. However, this is generally considered to be a small price to
pay for linear behavior with zero error as z approaches zero.
For any number N, the first constant k is given by
[7.31]
1
---N
k = 2 .
This is also the maximum range of the effective argument. In other segments, the value of k will be two half-segments larger. The maximum error,
as a function of the number of half-segments, is shown in Table 7.6.
Table 7.6
Order
1
3
0.026
8.395e4
1.022e3
3.635e6
2.216e4
2.838e7
8.083e5
5.283e8
3.805e5
1.504e8
202
Segments
Order
5
7
9
11
13
15
2.549e5
7.631e7
2.271e8
6.734e10
1.993e11
5.892e13
1.245e8
4.211e11
1.416e13
0
0
0
3.504e10
4.272e13
0
0
0
0
3.329e11
2.071e14
0
0
0
0
5.735e12
2.151e15
0
0
0
0
Minimax
One step remains to improve accuracy, and thats to adjust the coefficients
using a minimax approach as I did with the arctangent. To refresh your
memory, the error without minimax ordinarily is very low except at the
extreme of the range, at which it reaches a maximum. The idea of minimax
is to let the errors oscillate between bounds and accept larger errors in the
smaller values of z in order to reduce the worst case error. As I found with
the arctangent, the improvement in accuracy can be quite dramatic, easily
approaching or exceeding two orders of magnitude. As I perform the minimax process, I must decide whether or not to constrain the endpoint error.
Clearly, I can get a smaller worst case error by allowing it to occur at the
maximum range. However, I then must deal with the problem of discontinuous error curves where the segments meet. To avoid this, I can constrain the
error at the maximum, z, to be zero. This costs a bit in the worst case error,
but the trade is usually worth it in terms of having a well-behaved algorithm.
As an example, I can write the general form of the fifth-order approximation as
2
[7.32]
2z ( a + bz )- .
(z) = --------------------------2
1 + dz
[7.33]
a = 1,
b = 4/15 = 0.2666666667,
and
c = 9/15 = 0.6.
203
[7.34]
a = 1,
b = 0.2672435,
and
c = 0.600576.
The error curves before and after optimization are shown in Figures 7.5
and 7.6. In Figure 7.5, the two are shown to the same scale. Clearly, I get a
dramatic improvement in accuracy using the minimax process. Figure 7.6
shows how the optimized error tends to oscillate about zero.
The errors for the minimaxed case for various segments are shown in
Table 7.7. These errors were computed under the assumption of zero error
at the extremes. Errors with the end value unconstrained would be lower
yet. Note that for high-order approximations and large numbers of segments, the errors are too small for any of my methods to optimize. Ive
shown them as zero, but I know theyre really not. However, for practical
purposes, a zero indicates that these cases are so close to zero as not to matter and means that optimization is not necessary.
Figure 7.5
204
Figure 7.6
Minimaxed error.
Table 7.7
Order
1
3
5
7
9
6.736e3
5.646e5
4.044e7
2.953e9
0
2.56e4
2.361e7
1.978e10
0
0
5.54e5
1.781e8
5.387e12
0
0
2.023e5
3.394e9
5.148e13
0
0
9.515e6
9.447e10
0
0
0
Putting It Together
You now have the theoretical algorithm for any number of approximations
for the logarithm, giving acceptable accuracy over the limited range. What
remains is to turn the theory into usable code. For this purpose, Ill begin
with the assumption that the input argument is stored in C double format;
youll recall from the square root algorithm that this format is simpler than
the higher accuracy floating-point formats for Intel processors because of
the way the exponent is defined. I can use the same software hack that I
Putting It Together
205
used in the square root calculation to give separate access to the exponent
and mantissa of the floating-point word. The resulting code is shown in
Listing 7.1. For the purposes of this exercise Ive assumed a minimaxed
fifth-order formula, with five half-segments. As can be seen from Table 7.7
and Figure 7.6, this form provides more than ample accuracy for most
applications and may be considered overkill. Note that hacking into the
floating-point format allows me to force the argument into the range I
desire, neatly sidestepping the need to generate positive and negative return
values. This step is taken care of by the segmentation.
Listing 7.1
The ln algorithm.
/* Union to let us hack the exponent
* of a floating point number
* (also used in square root)
*/
typedef union{
double fp;
struct{
unsigned long lo;
unsigned long hi;
}n;
} hack_structure;
206
Putting It Together
207
Before leaving the natural log function, I should say something further
about bases. Youll recall that Ive talked about three useful bases:
10,
e, and
2.
Unless stated otherwise, Ive adopted the notation that log() refers to the
logarithm, base 10, and ln() refers to the same logarithm, base e. In my
implementation for the function, I have something of a dilemma. The power
series of Equation [7.15] refers strictly to the natural logarithm, ln(). Thats
because the series is derived from calculus formulas, and in calculus, the use
of the natural log is, well, natural. On the other hand, the exponent field of
the floating-point number Im processing is given as powers of two, so
clearly base 2 is more natural for that portion of the programming. So
which base should I use?
The decision is something of a toss-up, and for that reason, some computer systems and programming languages used to offer the logarithm, base
2, as well as base e and sometimes 10. However, remember that conversion
between the bases is trivially easy, as noted in Equations [7.12] and [7.13]:
ln xlog x = ----------ln 10
ln x- .
log 2x = -------ln 2
208
10 = 10
(n + f )
= ( 10 ) ( 10 )
x = 2 f,
where f is the mantissa constrained between 0.5 and 1. In taking the logarithm, you are really finding
n
ln x = ln ( 2 f ) ,
which is
[7.37]
ln x = ln ( 2 ) + ln f
= n ln 2 + ln f .
You can see that it really doesnt matter what the base is for the floatingpoint exponent (good thing, too, because some computers use base 4 or
base 16). You need only remember to multiply n by the natural log of whatever the base is. Its a simple point, but one thats easy to get tripped up on.
On to the Exponential
Now that you can compute logarithms, its time to focus your attention to
the other end: the antilog, or the exponential function. The power series is
given in Equation [7.14]. From experience with the sine/cosine series, you
should recognize that the presence of the factorial in the denominator
assures fast convergence of the series for any value of x, positive or negative.
Even if x is very large, the powers of x will eventually be overwhelmed by
the factorial in the denominator, and the series will eventually converge.
However, that doesnt necessarily mean that it will converge rapidly. To
make sure that happens, you need to take the usual step: restrict the range
of the input variable and, indirectly, the output variable.
Youll notice that the terms in the series of Equation [7.14] all have the
same sign. This is in contrast to, say, the series for the sine function, where
the terms alternate. You might think that one could get better accuracy by
On to the Exponential
209
restricting the argument to negative values, where the terms will alternate in
sign. That logic seems sound enough, but its wrong. This is partly because
the value of the argument can approach unity, which would make it cancel
the leading term of unity, and move all the significance to higher-order
terms. For this and other reasons, it turns out to be better to restrict the
range to positive values. When x is negative, the properties of exponents
give
[7.38]
x
1- .
e = -----x
e
The wrapper of Listing 7.2 takes care of this. In a moment, Ill show
you how to restrict the range much more tightly. Theres just one small
problem. Although Ive eliminated the negative half of the arguments
range, I havent limited the positive half, which can still go to infinity.
Listing 7.2
The series as shown in Equation [7.14] looks nice, but the form shown is
terrible for evaluating the series because it involves both factorials and successively higher powers of x. Instead, you should use Horners method, as
shown in Equations [7.39] and [7.40]. Here, Ive shown the method in two
forms. The first is the more common and is easier to write; the second is preferred because it uses less stack space an important consideration if your
computer is using a math coprocessor.
[7.39]
x
e = 1 + x 1 + --x- 1 + --3x- 1 + --x- 1 + --x- 1 + --x- ( )
2
4
5
6
[7.40]
x
e = ( ) --x- + 1 --x- + 1 --x- + 1 --x- + 1 --x- + 1 x + 1
5
4
3
2
210
x = n + f,
where n is the integral part of x, and f is a fraction with absolute value less
than one. Then, again as before, I can write
[7.42]
e = e
(n + f )
= (e )(e ) .
Listing 7.3
A practical exponential.
/* Generate the exponential from its Hornerized
* power series. 11th order formula gives 1e-8
* accuracy over range 0..1
*/
double exp2(double x){
return
((((((((((x/11+1)*x/10+1)*x/9+1)*x/8+1)*x/7+1)*x/6+1)
*x/5+1)*x/4+1)*x/3+1)*x/2+1)*x+1;
}
On to the Exponential
211
int n;
x = -x;
n = (int)x;
x -= n;
retval = exp2(-x);
intpart = pow(e, -(double)n);
return intpart*retval;
}
Though workable, this series still seems too long, so its natural to try to
reduce the range still further. In principle, thats easy. All I need to do is play
with n and f a bit. Instead of requiring n to define the integral part of the
input argument, I can let it be the number of any fractional segments in x,
[7.43]
n
x = --m- + y ,
where y is now restricted to the range 0 to 1/m. Clearly, I can reduce the argument to the exponential function to as small a range as I like simply by making m larger and larger. The only price I pay is the need to perform more
multiplications to handle the integral portion of the function as embodied
in n. The larger m is, the larger n must be to handle the integral part of the
function, so I can expect to perform more multiplications.
Using the new definition of x, as embodied in Equation (45), the new formula is
[7.44]
n
--- + y
m
1 n
---m- y
---m- y
= e (e ) = e e .
Note that the constant factor raised to a power is now the mth root of e.
Many programming languages support exponentiation. If I were programming in FORTRAN, Id use the built-in operator **. The C math
library includes pow(). You must be careful, however, how these functions
are used. Both are designed for the more general case, where a real number
is raised to a real power. Unfortunately, they do this by using ln() and
exp(), which I can hardly use here because I have to supply both functions.
In any case, these functions can be very inefficient when the second argument is an integer. Some compilers (notably FORTRAN compilers) test for
this condition and use different methods when the argument is an integer.
For small values of the argument, compilers will even convert the operator
212
Listing 7.4
On to the Exponential
213
to meet your accuracy requirements. Listing 7.5 shows the complete exponential function using m = 4 and a sixth-order power series. This combination gives a maximum error of 1.0e8, which should be sufficient for most
practical work. This routine is fast and accurate, and the code is simple.
Table 7.8
Order
1
2
3
4
5
6
7
8
9
10
11
Listing 7.5
16
0.718282
0.218282
0.051615
9.9485e3
1.6152e3
2.2627e4
2.786e5
3.0586e6
3.0289e7
2.7313e8
2.2606e9
0.148721
0.023721
2.83771e4
2.3354e5
2.3354e5
1.65264e6
1.02545e7
5.66417e9
2.81877e10
1.27627e11
5.30243e13
0.034025
2.77542e3
1.7125e4
8.4896e6
3.51584e7
1.24994e8
3.89223e10
1.07809e11
2.68674e13
5.77316e15
0
8.14845e3
3.35953e4
1.04322e5
2.59707e7
5.3943e9
9.61098e11
1.49925e12
2.08722e14
0
0
0
1.99446e3
4.13339e5
6.43814e7
8.03081e9
8.3529e11
7.44738e13
5.55112e15
0
0
0
0
In the
214
Listing 7.6
215
Minimaxed Version
As in the other series, I can optimize the coefficients to give a minimax
behavior over the chosen range. For a power series such as Equation [7.14],
I should be able to use Chebyshev polynomials for this. However, for this
function it seems more reasonable to minimax the relative, rather than
absolute, error. That takes me back to doing the minimax process, laboriously, by hand. Listing 7.6 shows the same function as Listing 7.5, but with
the order reduced to fifth order, and the coefficients optimized. Despite
using a lower order approximation, the error is now less than 2e10, which
ought to be enough for most any embedded application.
The Assignment
The assignment was for a company that already had existing products based
on the Zilog Z80 processor. They were in the process of converting everything to a new product based on the Intel 80486. My job was to convert a
major piece of the software from Z80 assembly language to C, then make it
work in our new hardware using our PC-based cross-development system. It
was important that the C-486 version exhibit exactly the same functionality
as the Z80 version no fair starting over from scratch. As is usually the
216
The Approach
When translating code from one CPU to another, there are a number of
approaches one can take. The fastest and least efficient method is to write a
CPU emulator and leave the application code alone. I could have written a
Z80 emulator for the 486, and simply executed Toms Z80 code on it, but
this would have been unacceptable for a number of reasons, not least of
which would be speed. Another serious problem would have been the difficulty of disconnecting the Z80 application code from the operating system buried inside it and reconnecting to the new multitasking OS.
A method closely related to emulation is to write an automated assembly-to-assembly translator. Ive written such translators in the past, and they
do work more or less well depending on how similar the two processors are.
An 8080-to-6800 translator produced code that was about three times
217
larger and ran about three times slower than the original. The difference
was primarily a result of the radically different behavior of the processor
flags. Before and after many of the 8080 instructions, I had to insert instructions to make the 6800 flags behave like the 8080s. For processors as similar as the 8080 and 8086, the performance hit would not be nearly so
drastic. For history buffs, this is the way Microsoft got their first versions of
Microsoft BASIC and other apps running on the IBM PC. They used an
80808086 translator provided by Intel to speed conversion from the 8-bit
to the 16-bit world.
Using an emulator or translator had certain appeal, not the least of
which was the fun and challenge of building the translator and/or emulator.
However, neither approach was considered acceptable in this case. First of
all, Id still have Z80 code or its equivalent in 8086 assembly language to
maintain. The cost of maintaining C code compared to assembly code
swings the decision strongly away from any kind of automated translation.
Most importantly, the product operated in medical equipment, where
people and their continued good health are involved. I couldnt afford not
to know, down to the last detail, exactly what was going on inside the system. I not only had to know the software inside and out, I had to understand what it was doing and why; therefore the translation involved as
much psychoanalysis as code conversion. To effect a thorough and robust
translation, I had to get inside Tom Lehmans head and understand not only
what he was doing in a code section, but what he had been thinking when
he wrote it.
I had to pull out every trick I could find in my toolbox, from flowcharts
and pseudocode to structure diagrams, call trees, branch trees, data dictionaries, and some new angles I invented on the fly. One of the more powerful
techniques was using a good macro editor to perform semi-automatic translations.
The Function
Translating software in this way is much like disassembling. Usually, beginning at memory location zero and working forward is a Bad Idea. Instead,
you work in a bottom-up process. You look for low-level functions that are
easily broken out from the main body of the software and figure out what
they do. Once youve figured those out, you assign meaningful names to the
functions and their related data items, then you look for the places theyre
called, which often gives you a hint as to the functionality of the next level
up. The only difference, really, between translating and disassembling is that
218
B ( x ) = 8 ( b 1 ) + n .
Because the input number was 16 bits, the largest value b can have is 15,
and the largest n is seven. The range of the output number is 0 through 119,
which fits comfortably in a single byte.
What was going on? Despite the seemingly obvious name of the function,
it looked like some kind of floating-point format. I have worked with software floating-point routines and had, in fact, just finished translating Toms
short (24-bit) software floating-point package, which in turn was very similar to the one Id written 20 years earlier and presented in a 3-part magazine
column at the end of 1995 (Crenshaw 1995-96). The operations in Bitlog
looked very much the same, with b playing the role of an exponent and n
the lower three bits of a four-bit mantissa. This concept was also familiar to
me. In floating-point numbers, any number except zero always has the high
bit of the mantissa set. This being the case, theres no point in storing it.
Instead, you sometimes gain an extra bit of resolution by storing only the
bits after the most significant bit and assuming the highest bit is always a 1.
This is called the phantom bit method. Could Tom possibly have invented
not only a 24-bit floating-point format, but an eight-bit one as well? For
what purpose?
Try as I might, I couldnt find any place where arithmetic was done using
this format. In fact, in the only place I found the output of Bitlog used, it
seemed to be used as a single integer.
219
What could this mean? Why take a number apart, store different parts of
it in different ways, and then treat the whole thing as though it was a single
integer?
Fortunately, I didnt have to worry about the deeper meaning. As I got
further into the software translation, I realized that Bitlog was used only for
output, and I was replacing the output portions of the software with totally
different software, reflecting the differences in display technology. So for a
time, at least, I set Bitlog aside as a curious oddity and went about the business at hand. Later, however, when I had more time to dig into it, I resolved
to revisit the function and figure out what the heck Tom was doing there.
Table 7.9
Binary
Output
61440
1111xxxxxxxxxxxx
119
57344
1110xxxxxxxxxxxx
118
53248
1101xxxxxxxxxxxx
117
49152
1100xxxxxxxxxxxx
116
45056
1011xxxxxxxxxxxx
115
40960
1010xxxxxxxxxxxx
114
36864
1001xxxxxxxxxxxx
113
32768
1000xxxxxxxxxxxx
112
30720
0111xxxxxxxxxxxx
111
28672
01110xxxxxxxxxxx
110
220
Input
Binary
Output
26624
01101xxxxxxxxxxx
109
24576
01100xxxxxxxxxxx
108
22528
01011xxxxxxxxxxx
107
20480
01010xxxxxxxxxxx
106
18432
01001xxxxxxxxxxx
105
16384
01000xxxxxxxxxxx
104
Not only does the high half of the word change like the logarithm,
according to the position of the highest nonzero bit, but the low part
increases by one for every significant change in the next three bits. This cant
possibly be equivalent to the logarithm, since the change is linear over the
range of the lower bits. How far off is it? To see, I plotted the data shown in
Table 7.9. The results are shown in Figure 7.7, where Ive omitted the values for the smallest values of the argument.
For better comparison, Ive added a line corresponding to the logarithm,
base 2, of the input. As you can see, the two lines seem to have the same
slope when plotted logarithmically. The offset, for the record, is eight, so I
can make the correspondence even more dramatic by plotting the Bitlog
along with the function.
[7.46]
g(x) = 8 [ log 2( x ) 1 ]
Figure 7.7
221
bitlog (x)
log2 (x)
This graph is shown in Figure 7.8. The results are stunning. To the scale
of Figure 7.8, the two functions are virtually indistinguishable. To put the
final nail in the coffin, I looked at the error curve, shown in Figure 7.9. As
you can see, the error is always less than one in the lowest bit, which is about
all you can expect from an integer algorithm.
222
Figure 7.9
Error behavior.
Error
Figure 7.8
223
There still remains the behavior of the function near zero. The floatingpoint version of the log function has the embarrassing and sometimes frustrating tendency to go to negative infinity when the input argument is zero.
With a little care, I can define a function behavior that is not so nasty, but to
do so, I need to get a bit creative for the smaller values. The values of B(x)
for arguments of 32 and less are shown in Table 7.10.
Table 7.10
Binary
B(x)
32
0000000000100000
32
31
0000000000011111
31
30
0000000000011110
31
29
0000000000011101
30
28
0000000000011100
30
27
0000000000011011
29
26
0000000000011010
29
25
0000000000011001
28
24
0000000000011000
28
23
0000000000010111
27
22
0000000000010110
27
21
0000000000010101
26
20
0000000000010100
26
19
0000000000010011
25
18
0000000000010010
25
17
0000000000010001
24
16
0000000000010000
24
15
0000000000001111
23
14
0000000000001110
22
13
0000000000001101
21
12
0000000000001100
20
11
0000000000001011
19
224
Input
Binary
B(x)
10
0000000000001010
18
0000000000001001
17
0000000000001000
16
0000000000000111
3?
14
0000000000000110
2?
12
0000000000000101
1?
10
0000000000000100
0000000000000011
1?
0000000000000010
0?
0000000000000001
0?
0000000000000000
But this is incorrect. It would imply a 0 between the leading 1 and the
next two bits a 0 that isnt really there. To follow the algorithm to the letter, I must take the value of n to be 110 binary, or six decimal, to give
[7.48]
225
In this way, the function extends all the way down to x = 0 and gives a
reasonable result: zero. Although this result may hardly seem reasonable for
a logarithm function and may not make sense from a strictly mathematical
point of view, its certainly a lot more comfortable than negative infinity,
and the end result is a function thats well behaved while also close to logarithmic over its full range, as shown in Figure 7.10 of the complete function.
As expected, the function perfectly parallels the logarithm down to x = 8, at
which point the Bitlog function flattens out to arrive at zero for zero input,
while the true logarithm continues its inexorable slide to minus infinity.
Figure 7.10
bitlog (x)
g ( x)
For those of you who like analogies, consider this one. Its a well-known
fact that human hearing is logarithmic in nature; a sound that contains double the power doesnt sound twice as loud, only a fixed amount (3dB)
louder. Accordingly, potentiometers that are used as volume controls are
designed to generate a logarithmic response; turn the knob through x
degrees, and you get y more decibels. If youre ordering a potentiometer
from a vendor, hell offer your choice of linear or logarithmic response
curves. All audio amplifiers that still use analog components use logarithmic
pots for the volume controls. But this rule clearly cant go on forever,
226
The Code
Now that you know the rules, the implementation (Listing 7.7) is fairly
straightforward almost trivial. To make the function more universal, I
decided to use an unsigned long integer as the input argument. The output
value, 247 for an input of 0xffffffff, still fits comfortably in a single byte,
much less the short Im returning.
Listing 7.7
Listing 7.7
227
Because its easier to test a bit for nonzero value if you know where it is,
I chose to shift left instead of right. This way, I can always test the sign bit
for a 1. Once its been found, I strip it off, move the three bits back down
where they belong, and build the result.
The only tricky part is to remember to stop shifting when I reach what is
ultimately to become bit 3. I tried a few tricky ways to do this by monitoring the value of b, but then it dawned on me that for input arguments less
than or equal to eight the output is simply twice the input. So I short-circuit
the entire calculation for these cases, which simplifies the code and is faster
for small inputs.
x - + ---x - ---x- + .
ln ( 1 + x ) = x ---2 3 4
228
[7.50]
Naturally, the more terms in the series, the more accurate its likely to be.
On the other hand, this power series is notoriously slow to converge. The
denominator of each term doesnt contain a factorial or any other mechanism that makes the fraction get small very rapidly. It increases only as n, so
you cant expect much difference in size between the 1,000th and the 1,
001st term. I do not show the graphs here, but trust me, you dont get much
improvement by going from the first-order approximation to the fourth.
Hence you might just as well use the first-order approximation, and write
[7.51]
ln ( x ) = x 1 .
This curve is shown in Figure 7.11 over the range 1 to 2 along with the
natural log function Im trying to fit. The results are pretty terrible. The
curves start out together and agree closely for small x, but the approximation curve, being linear, continues in a straight line while the logarithm
function curves downward. At x = 2, the error is considerable.
Figure 7.11
Linear approximation.
However, I can improve the fit dramatically by using a scale factor in the
approximation. Instead of requiring that the two curves be parallel for small
z, Ill change the function to
229
(z) = C (z 1)
C = ln ( 2 )
( z ) = ln ( 2 ) ( z 1 ).
This curve is plotted along with ln(z) in Figure 7.12. As you can see, the
fit is much better. More dramatic is the error curve in Figure 7.13. It should
look familiar because its the same shape as the error curve of the Bitlog
function (Figure 7.9).
Figure 7.12
230
Figure 7.13
One more step needs to be taken. Recall that the Bitlog function is
related to the logarithm, base 2, not the natural log. The two are related by
the formula
[7.55]
ln ( x ) .
log 2( x ) = -----------ln ( 2 )
log 2( x ) = x 1 .
In short, most of the error seen in Figure 7.13 goes away if you use the
approximation not as the logarithm base e, but the logarithm base 2.
Now you can begin to see where the Bitlog function comes from and why
it works so well. It is, in fact, the first-order approximation of the base 2
logarithm, give or take a couple of constants. Imagine a four-bit number in
which the high bit is always a one, and the binary point immediately follows
it:
1.zzz.
The lower three bits can vary between 0 and 7, giving a fractional part
between zero and 7/8. To get z for this approximation, I must subtract one,
which is equivalent to stripping the high bit. Thats exactly how the Bitlog
function works. From my high school days, I know that I can deal with inte-
231
gral parts of the log simply by shifting the number left. This is the function
of the b component of the Bitlog. The factor of eight is simply to get over
the three fractional bits.
Only one question is unresolved: Why subtract 1 from b? Why is the
function
g(x) = 8 [ log 2( x ) 1 ]
The short answer is, if I didnt subtract 1, I would not get a function that
passes near zero at x = 0. More to the point, however, note that the smallest
value I can operate on is the binary number that still has a 1 before the decimal point:
1.000.
In order to take the number all the way down to zero, I must assume an offset of one. I must admit, this additive factor is a bother. If I want to do arithmetic using the logarithm, I need to be able to revert Equation [7.46] to read
[7.57]
1
log 2( x ) = --- B(x) + 1 .
8
Adding 1 is an extra step Id rather not have to do. I can always redefine
the Bitlog to come closer to the true logarithm, base 2, but to do so Id have
to give up the nice behavior near zero. Im not ready to do that, so the Bitlog
function stands as Tom Lehman wrote it.
232
E(z) = B (z)
so that
[7.59]
E ( B ( x ) ) = B ( E ( x )) = x ,
at least within the limits of integer arithmetic. Now consider the following.
B(x) 8 [ log 2( x ) 1 ]
2
B( x )
8 [ log 2( x ) 1 ]
1 - 8 log 2( x )
= ---2
2 8
Then
[7.60]
B(x)
8
--2x- .
x = B ( z ) = E(z) .
z+8
[ E(z) ]
E(z) 2
[7.62]
E(z) 2
z + 8---------8
z
--- + 1
8
This, then, is the relationship between the inverse Bitlog function (the
Bitexp), and the exponential function. Calling the integer exponential is
233
B ( E ( x )) = x .
The opposite is not quite true because B(x) generates the same values for
different inputs. However, the two functions do revert within the limitations
of integer arithmetic, which is the best one can hope for.
Listing 7.8
Integer exponential.
/* Bitexp function
*
* returns an integer value equivalent to
* the exponential. For numbers > 16,
*
* bitexp(x) approx = 2^(x/8 + 1)
*/
unsigned long bitexp(unsigned short n)
{
unsigned short b = n/8;
unsigned long retval;
// make sure no overflow
if(n > 247)
return 0xf0000000;
// shorten computation for small numbers
if(n <= 16)
234
Listing 7.8
Integer exponential.
return (unsigned long)(n / 2);
retval = n & 7;
retval |= 8;
cout << dec << b << ' ' << hex << retval << endl;
return (retval << (b - 2));
}
Wrap-Up
I hope youve enjoyed this excursion into the Bitlog function and its origins.
As with the audio volume control mentioned earlier, its useful whenever
you need a response thats roughly logarithmic in nature. Many such situations exist. A typical case is when you need a function similar to an automatic gain control (AGC). That was Tom Lehmans original use. Im
currently using the function in a different context, in a production program, and it performs flawlessly. Its blazingly fast and more than accurate
enough for the application.
Postscript
Since I first published the Bitlog function and its inverse in my column in
Embedded Systems Programming magazine, I received several e-mail messages from folks who were using similar algorithms. All were very pleased
with the algorithm, and most thought they had invented it (and in a sense,
they did). Apparently, there is more than one genius out there.
References
Crenshaw, J. W. Programmer's Toolbox: Floating-Point Math, Embedded
Systems Programming, Vol. 8, #11, November 1995, pp. 25-36.
Crenshaw, J. W. Programmer's Toolbox: Floating-Point Math, Part 2,
Embedded Systems Programming, Vol. 8, #12, December 1995, pp. 2940.
Crenshaw, J. W. Programmer's Toolbox: Floating-Point Math, Part 3,
Embedded Systems Programming, Vol. 9, #1, January 1996, pp. 19-34.
Section III
Numerical Calculus
235
236
8
Chapter 8
I Dont Do Calculus
When I began this book, I wrestled with the problem of how far I needed to
go into calculus. That I was going to have to get into it, I had no doubt. The
reason is simple: embedded systems are the bridge between two worlds
the discrete world of digital computers, with their loops, tests, and
branches, and the analog world that we live in. The digital computer obeys
programmed commands, predefined by its builders to perform certain mathematical or logical operations, in sequence as written by the programmer
and scheduled by the CPU clock circuitry. The real world, on the other
hand, is an analog one. Events change with time, and the behavior of such
events is described by calculus specifically, according to the laws of physics. Those laws, like it or not, are almost exclusively described by differential equations.
Any embedded system more complex than a television has sensors that
go out and measure some of these real-world events. It often controls actuators whose job it is to alter these events, so again, even though one may
wish it to be otherwise, the behavior of real-world events is described by
calculus. Every feedback loop, every PID controller, every low-pass or highpass filter, every simulation (even your favorite action game), and every fast
Fourier transform (FFT) is a case of calculus in action.
237
238
f (x) sin ( nx ) dx .
239
Galileo Did It
Long before Newton got beaned by that apple, people wondered what was
going on when things fell. The first person to get it right was not Newton,
but Galileo Galilei in 1638. He did something that had been considered bad
form by the Greek culture before him: he performed an experiment. He
wanted to time falling bodies to see how far they fell in each time interval.
To slow them down enough to observe, he rolled balls down a grooved,
slightly sloping board and timed their motion. He came up with a remarkable (for his time) conclusion: the speed of a falling body increases at a constant rate, independently of its mass. He proved it to the skeptics by that
famous experiment, which may or may not have happened, at the Leaning
240
v = gt.
Figure 8.1
[8.2]
v
g = -t
Unfortunately, Galileo didnt have Figure 8.1. Because radar guns hadnt
been invented yet, he couldnt measure speed directly only position and
time. What he had, then, was a graph like that in Figure 8.2. However, he
could deduce the relationship between speed and time through a simple but
ingenious line of reasoning, as follows. Look at Figure 8.1 again. Choosing
some arbitrary time t, indicated by the vertical line, how far can I expect the
object to move during that time? If the speed is constant, that would be
easy: its simply that speed multiplied by the time. But because the speed is
Galileo Did It
241
not constant, I have to use the average speed. Because the speed is changing
linearly with time, that average speed is halfway between zero and the current speed. In other words, its v/2, so the distance traveled, x, must by the
product of this average speed and t:
1
x = ( v 2 )t = --- ( gt )t
2
or
[8.3]
Figure 8.2
1 2
x = --- gt .
2
A falling body.
Thats exactly the kind of curve Galileo saw in Figure 8.2, so all he had
to do was fit the data to find the value of g.
One important aspect of all this is that the parameters in Equations [8.1]
to [8.3] are not dimensionless; they have units. The acceleration g has units
ft/s2. Multiplying by t, as in Equation [8.1], gives the units (ft/s2)s = ft/s, the
units for speed. Similarly, the units in Equation [8.3] are (ft/s2)s2 = ft, the
units for position. Notice that the units cancel, just as if they were algebraic
variables. You may not have seen units bandied about this way, but it
works, and in fact, you manipulate units the same way whenever you compute an average speed or a trip time.
242
Galileo Did It
243
need to do a little more work before the notation makes much sense. For
now, just remember the following.
The area under the curve (x) is called the integral of (x).
Before going any further, I need to clarify the term, area under the
curve, because I recall that it left me feeling less than confident the first
time I heard it. I mean, when people say under the curve, just how far
under do they mean? And because the curve can extend from zero to
infinity (or sometimes to negative infinity), isnt that area also infinite?
Questions like this used to bother me in college, and left me with the nagging feeling that I was missing something, and I was.
Two points help clear up any ambiguities. First, saying under the curve
is somewhat sloppy terminology. What is really meant is the area between
that curve and the x-axis [i.e., the axis for which (x) = 0]. Whats more,
area is defined in a way thats not very intuitive to the layman. In this terminology, any time the curve is above the x-axis, the area is positive. If it
goes below the axis, that area is considered negative (because its a negative
value multiplied by the width of the section). A curve like the sine wave can
range equally above and below the x-axis, and end up with a total area of
zero. That concept takes some getting used to, but it is critical in understanding the terminology.
Second, not all the area under the curve is included, only the area from
one specified point (often x = 0) to another specified point. In other words,
the area under the curve is a thoroughly bounded area delimited above by
the curve, below by the x-axis, to the left by the starting value of the independent variable, and to the right by the ending value of the independent
variable.
244
1 2
x(t) = v 0 t + --- at ,
2
where v0 is the initial speed. You can do similar things if the position or the
time doesnt start at zero. The most general forms for the equations are
[8.5]
v(t) = v 0 + a ( t t 0 )
and
[8.6]
1
2
x(t) = x 0 + v 0 ( t t 0 ) + --- a ( t t 0 ) .
2
This is a good place to discuss the difference between definite and indefinite integrals. Its a distinction that often causes confusion, and I wish someone had been around to clarify the difference for me.
There are two ways to look at Figure 8.1. First, imagine the time t to be a
general parameter, starting at zero and increasing as far as you like. As t
increases, imagine the vertical line sweeping out a larger and larger area,
and if you plot that area against time, you get a new function x(t), as shown
in Figure 8.2. In other words, t is a dynamic parameter that takes on a range
of values, generating x(t) as it does so. This is the indefinite integral, so
called because it holds for all values of t when you havent specified a particular value.
On the other hand, you can think in more static terms. Imagine two specific values, t1 and t2, which delimit areas in Figure 8.1. In this case, the area
is not a function, because nothing changes. Its simply a number to be
found, and you can do so by subtracting the area for t1 from that for t2.
Alternatively, since Figure 8.2 already represents these areas for all t, you
can simply use it to calculate the difference x(t2) x(t1). This is the definite
integral. Therefore, the essence of the relationship between the two kinds of
integrals is that
the indefinite integral is a function and
the definite integral is a number.
To get the definite integral of any function, evaluate the indefinite integral at
the desired two points and take the difference. Simple.
Galileo Did It
245
A Leap of Faith
Getting back to Galileos problem, recall that the speed and position are
related to acceleration as areas under curves. But now Ive identified this
area under the curve as the integral of a function. At this point, I bid farewell to Galileo, who only considered the case of constant acceleration. Im
going to make a giant leap of faith here and assert without proof that the
same kind of relationship works for any kind of acceleration. In other
words,
v(t) = integral of a(t), and
x(t) = integral of v(t).
Figure 8.3 shows an example for a more complicated case. For the
moment, dont worry about how I calculated these integrals. Simply notice
that, although its not obvious that the three curves are related through integrals, they do seem to hang together. When acceleration is high, the speed
increases most rapidly. When its constant, the speed increases linearly, just
as in Galileos problem. When the acceleration is zero, the speed is constant.
Also note that when the speed is zero, the curve for position is flat; that is,
the body is standing still.
Everything seems to boil down to this idea of measuring the area under
the curve. In the case where the curve is a straight line, its trivially easy. It
gets harder as the function to be integrated gets more complex. Its a lot easier if you can write down an analytical expression describing the curve; the
problem of finding an analytical integral, given such an expression, is the
essence of integral calculus. Ill touch on this later on, but Ill warn you of a
depressing fact in advance: the set of functions that cant be integrated analytically is infinitely larger that the set of those that can. Often, you have to
resort to numerical or other techniques to estimate the area under the curve.
246
Figure 8.3
247
y2 y1
m = ---------------.
x2 x1
Mathematicians and physicists are a lazy lot, so whenever they see the
same form popping up over and over, they tend to create a shorthand notation for it. In this case, the notation for the difference between two adjacent numbers is
[8.8]
x = x2 x1
y
slope = m = ------ .
x
Get used to this delta notation because youll see a lot more of it.
According to this definition, as well as intuition, the slope of a horizontal
line is zero. The slope of a line at a 45 angle is one because x and y are
equal. The slope of a vertical line is infinite because the denominator, x, is
248
Figure 8.4
If I hadnt told you the value of g, you could have easily gotten it by plotting a graph as in Figure 8.1 (from Galileos data, perhaps). At the end of
one second, youd find that the speed is 32 ft/s, so by definition the slope is
32 ft/s2. But thats exactly the value of acceleration. If you were to plot Figure 8.2 with a scale and measure the slope by using the tangent lines, youd
find that its always exactly the same as the speed at that instant. This is just
like the integral relationship, except backwards. Not surprisingly, mathematicians have a name for this slope, too.
Symbolism
Figure 8.5
249
Symbolism
So far, Ive studiously avoided showing you calculus notation. I thought it
more important to start with the concepts. But its time now to look at the
symbolic forms used to indicate concepts like integral and derivative.
250
Over time, the uppercase Greek delta () has become universally recognized by mathematicians to imply a difference of two numbers. In the discussion of slopes, Ive been deliberately vague about just what points should
be used for (x1, y1) and (x2, y2), partly because, for straight lines, it doesnt
really matter. Similarly, if youre measuring the slope of a curve graphically,
it doesnt matter; once youve drawn the tangent line, as in Figure 8.4, you
can pick any two points on the line to do the deltas.
Imagine that someone gives you a mathematical function, (x), and asks
you to plot its slope over some range in x. How would you do it? The obvious approach would be to first plot the graph, to as high an accuracy as you
can, then draw tangent lines at various places. You could determine the
slope of each of those lines using the method Ive discussed and plot those
slopes against x.
Of course, drawing the tangent line to a curve is not the easiest thing in
the world to do. Ive done it many times, and one can get a reasonable
result. But after all, the accuracy of that result is only as good as the accuracy with which you can draw. (I feel compelled to add here that Im old
enough to remember when an engineers skill with drafting instruments was
at least as highly valued as the quality of his Table of Integrals. Before Quattro Pro and Excel, engineers used a drafting table, a T-square, French
curves, and, among the most useful gadgets of all, an adjustable triangle,
which not only allowed you to draw a line at any angle, but also to measure
one to a fraction of a degree. If this strikes you as primitive and crude, let
me remind you that its this kind of thing that got us to the Moon. Those
were the good old days.)
Although your results using this approach might be crude, you can certainly imagine, in your minds eye, improving the accuracy to any desired
degree of precision. All you need to do is simply change the scale of your
graph. You could pick a tiny section of the curve of (x), plot it on as large a
table as you need, draw the tangent line, and measure its slope as carefully as
possible. Its as though you took a magnifying glass and expanded each section of the curve, plotting it to whatever accuracy you need.
Conceptually, you could continue this process indefinitely until you get
the desired accuracy. Its a tedious process, but if, for example, the survival
of the free world were at stake (as it was during World War II) or the success
Symbolism
251
of the Apollo program were at stake (as it was shortly afterward), youd find
a way to get those numbers.
In any case, regardless of how difficult it really is to do something, mathematicians have no problem envisioning the process going on forever. They
can imagine infinitely wide graph paper and infinitely accurate plotting of
the function and its tangent line. They can envision a magic magnifying
glass that lets them blow up any section of the graph without losing resolution. Eventually, if you continue this process, the curve (x) becomes indistinguishable from a straight line and the task becomes trivial.
Now, Ill change the experiment a little bit. If you are using a computer,
you do not have a graph; all you have is an equation for the function (x).
Because theres no computer instruction called find a tangent to a curve,
you have to resort to an approximation, as in Figure 8.5. If I want to know
the slope at point P1, I could (rather foolishly) choose the right-hand point,
P4, do the delta thing, and find the slope using Equation [8.9]. This gives the
slope of the straight line connecting the two points, and that slope is indeed
approximately the same as the slope at P1. (This method, by the way, is
called the method of divided differences and is often used for computer
work when no other data is available.) As you can see, its only a rough
approximation. Because I chose a point rather far from P1 and the curve has
considerable curvature over that region, the line is obviously not really tangent to the curve. Choosing P3 gets me closer and P2 closer yet. At this
point, the eyeball says that the two slopes are just about the same, but
numerically I still have only a few digits of accuracy.
Using a computer, Id have a problem getting a much better result. I can
continue to move the second point closer to P1, and, for a while at least, I
will continue to get better results. Unfortunately, I will also begin to lose
accuracy because, eventually, the numbers get so nearly equal that their difference comes close to zero. The floating-point numbers used by computers
are not infinitely accurate, and computers dont take kindly to being asked
to compute the ratio 0/0.
Nevertheless, even though any computer, as well as any graphical
method, eventually runs out of gas in this problem, mathematicians have no
such limitations. Their magic magnifying glass works forever, and they have
no trouble at all imagining the second point getting closer and closer to P1
until its infinitesimally close, at which point the computation of the slope
would also be infinitely accurate. As my major professor used to say, Conceptually, theres no problem. To indicate this downhill slide to infinitesimal sizes and infinite resolution, Ill change the notation to read
252
[8.10]
dy
slope = m = ----- .
dx
y = (x) = x .
x2 = x1 + ,
y 2 = x 1 + 3x 1 + 3x 1 + .
Then
3
y = y 2 y 1 = x 1 + 3x 1 + 3x 1 + x 1 ,
2
= 3x 1 + 3x 1 + ,
and
2
3x 1 + 3x 1 +
y
m = ------ = ------------------------------------------x
or
[8.14]
y
2
2
------ = 3x 1 + 3x 1 + .
x
Symbolism
253
To complete the process, you need one last leap of faith, but at this point
its not a very large one. As you can see, the first term in Equation [8.14]
doesnt contain , but the others do. It should be apparent that, as I shrink
to get closer and closer to the desired slope, these terms are going to contribute less and less and will eventually vanish, sort of like the Cheshire cat. In
fact, now that I have Equation [8.14], I can simply set to 0 and declare
that
[8.15]
dy
2
----- = 3x .
dx
This is the essence of differential calculus, and youve just seen a derivative calculated. Remember, I didnt say anything at all about which value of
x I used to find the derivative, so this result is perfectly general it applies
everywhere along the function (x) = x3. Finding the derivative of a function
is called differentiation.
Big-Time Operators
So far, Ive always used the symbols and d as part of a larger term like x.
But its also possible to think of them as operators, just like the unary
minus. Applying to x, for example, always means taking the difference of
two adjacent values of x. Similarly, taking a derivative of any function with
respect to x means computing the slope of the function, as I did to get Equation [8.15], so you might see the operators standing alone with nothing to
operate on; for example, d/dx or d/dt. As long as everyone agrees what is
meant by these operators, thats fine. In fact, to some extent the terms in this
fraction can be manipulated as though they were algebraic variables. For
example, I can talk about the naked differential dy and write
[8.16]
dy
dy = ----- dx .
dx
Some Rules
I can use this concept of a derivative and compute the derivative for any
given function in exactly the same manner that I did above for (x) = x3.
Recall that I got the result for that case by expanding a power of x1 + ,
using a binomial expansion. For the polynomial term, the leading term in
the expansion was canceled, and the second term was 3x2 multiplied by ,
254
The rule even works when n = 0. There are whole books that give the
derivatives of various functions. I obviously cant show all the derivatives
here, but fortunately only a few will cover most of the cases youre likely to
encounter. The key ones are shown in Table 8.1. Also shown are a few rules,
similar to the associative/commutative rules of algebra, that define what to
do with combinations of various forms. Armed with these formulas and a
little practice, youll find that there are not many expressions you cant differentiate.
Table 8.1
Derivatives.
Derivatives of Functions
d n
n1
x = nx
dx
d
k = 0
dx
(k any constant)
d
sin x = cos x
dx
d
cos x = sin x
dx
d x
x
e = e
dx
d
1
ln x = --dx
x
Getting Integrated
255
Combination Rules
d
df
k f = k -----dx
dx
d
d f dg
( f + g ) = ------ + -----dx
dx dx
d
df
dg
( fg ) = g ------ + f -----dx
dx
dx
d f
1 df
dg
--- = ----2- g ------ f ------
d x g
d
x
d x
g
Its important to realize that none of the rules are black magic, and you
dont need to be Einstein to derive them. They can all be derived using the
same method I used for finding the derivative of (x) = x3. However, you
dont have to derive them, because someone else already has. A table of integrals (or derivatives) is not something that was handed down from the sky
on stone tablets; its simply the tabulation of work done by others.
Getting Integrated
So far, Ive been pretty successful at explaining the concepts of calculus by simple examples and with a modicum of common sense. Now Ill attempt the
same thing with integrals.
Ill go back to the idea of the integral as the area under a curve. Just as
the definition of a derivative came from trying to find the slope of a curve,
you can understand integrals by trying to find the area under it. Look at Figure 8.6.
Suppose you want to know the integral of (x), the area under the curve,
from zero to some arbitrary value x. To approximate this area, divide the
distance x into N intervals, each having length
[8.17]
x
x = ---- .
N
For each such interval x, you can construct a rectangle whose height
touches the curve at the top left corner. To approximate the area, all you
have to do is to add up the areas of the rectangles. In other words,
[8.18]
256
Figure 8.6
I can simplify all this by using the notation for summing terms:
N1
[8.19]
integral =
(xi)x .
i=0
Of course, x is the same in all terms, so by all rights, you could factor it
outside of the summation. For where Im going, though, its better to leave it
where it is.
As you can see from Figure 8.6, this kind of estimate is pretty awful when
N is small; that is, when x is large. But I can fix that easily enough the same
way I did when estimating the slope in Figure 8.5: by reducing the size of x.
As I do so, the number (N) of terms inside the summation gets larger and
larger. At the same rate, x is getting smaller, so the sum, which contains x
as a factor, remains bounded. Eventually, I get to the point where x has
earned the right to be called the little bitty value dx.
Getting Integrated
257
Figure 8.7
258
[8.20]
integral =
(x) dx .
This is an indefinite integral, which means that it is a function. Remember that, as in Figure 8.1, Im talking about the area under the curve from 0
to some arbitrary value of x. A definite integral looks much the same, except
that in this case Im talking about a specific number, which is the area
enclosed by two bounding values of x. To indicate the definite integral, I can
annotate the integral sign with the range information, like this:
4
[8.21]
I =
x dx .
2
More Rules
I now have a notation for the integral that makes a certain sense when you
see how it was developed from a summation of areas. Whats still missing is
a set of analytical rules for computing the integral. After all, you cant just
add up a million terms every time. If at all possible, it would be better to
have a set of rules similar to those in Table 8.1.
As you might recall, I was actually able to derive one of those rules analytically by expanding a power of x + in a binomial expansion. Unfortunately, I cant do similarly for integrals. Fortunately, I dont have to.
Remember the duality between derivatives and integrals. The derivative of
the integral of (x) is (x) again. So to determine what the integrals are, I
need only ask what function will, when differentiated, yield the function Im
trying to integrate. Recall, for example, Equation [8.15].
d 3
2
x = 3x
dx
The answer, then, is that the function that yields x2 as a derivative is simply
one-third of x3; in other words,
[8.22]
1 3
dx = --- x
3
1 n+1
dx = ------------ x
.
n+1
This and some other useful rules are shown in Table 8.2.
Getting Integrated
Table 8.2
259
Integrals.
Integrals of Functions
dx
x
= x
1 n+1
dx = ------------ x
n+1
dx
-----x
(n 1)
= ln x
e dx
x
= e
sin x dx
= cos x
cos x dx
= sin x
Combination Rules
k f ( x ) dx
( u + v ) dx
u dv
= k f ( x ) dx
=
u dx + v dx
= uv v du
Now you can see how I knew what the correct value should have been in
Figure 8.7. I simply evaluated Equation [8.21] as a definite integral. Youve
heard me say that to evaluate a definite integral, you evaluate the function
at the two extreme points and subtract them. You may never have seen it
done or seen the notation used to do it, though, so Ill walk through it here.
4
1 34
1 3
1 3
64
= --- ( 4 ) --- ( 0 ) = -----dx = --- x
3 0
3
3
3
The funny parallel bars that look like absolute value enclosures mean, in
this context, to evaluate whats inside for the two limits shown; that is, substitute both limits into the expression and subtract the value at the lower
limit from the value at the higher limit. Its easier to do it than to describe it.
260
Some Gotchas
Id like to be able to tell you, as with Table 8.1, that most of the integrals
youll need can be found in Table 8.2. I cant. There are lots of forms that can
be integrated, and each form is rather special. Thats why whole books of
tables of integrals are published. If you dont find what you need in Table 8.2,
its time to go to the nearest library.
Youll also notice that there are not nearly as many helpful rules, so if
you dont find the exact form you need, you wont get much help from
them. Face it: integration is tougher than differentiation.
If possible, its always best to get an indefinite integral because its an
analytical function and so is valid for all x. To evaluate a definite integral,
you then need only plug the range values into the equation. Failing that, you
are often forced to resort to such tricks as counting squares or weighing
graph paper. But you dont need such tricks anymore because everyone has
computers now, right?
Because this subject is so important, a lot of methods have been devised
for computing numerical solutions, and they dont require using a million
terms. Ill be talking about such numerical methods in Chapter 9.
One other tricky little bit to cover is the strange case of the additive constant. In Table 8.1, youll see that the derivative of a constant is zero. The
significance of that comes in when you consider what function will, when
differentiated, give the function youre trying to integrate. Ive given the
integration formula as though I could answer the question, and all those
books of tables, including Table 8.2, reflect that. But really, you cant
answer the question that precisely. If some constant was added to the function, you wouldnt know based on the derivative because that information
gets lost in the process of differentiating. To be perfectly precise, the best
you can say is that the integral is thus-and-so plus a possible additive constant. The constant is not shown in the tables, but you should always
assume one. For definite integrals, this is not a problem. Even if there were
such a constant, it would cancel during the subtraction process. But for
indefinite integrals, always include an additive constant as part of the
answer. What value should it have? Often its decided by the boundary conditions for the problem. For the falling body problem, for example, the constant shows up in Equations [8.5] and [8.6] as v0 and x0. Its especially
important to note how v0 appears in the formula for x. Because of the second integration, its no longer just a constant theres a time-dependent
term that you would have missed without it.
Getting Integrated
261
u dv
= uv v du .
This is the first time Ive mentioned an integral over any variable but x.
Dont let it throw you. After all, you can call the variables by any name you
choose, but theres an even more important, and much more subtle, implication to Equation [8.24]. In it, assume that u and v are themselves functions
of x. But remember that you can treat these d operators as though they are
algebraic, so you can write the chain rule
du
du = ------ dx
dx
(and similarly for dv). What Im really talking about here is a change of
variables. If you cant find the integral that you need in Table 8.2 or in a
handbook, you still often can solve the problem by substitution of variables.
Sometimes, when faced with a particularly nasty integral, its possible to
separate it into the two parts: u and dv. If dv can be integrated to get v, you
can use Equation [8.24] to get an equivalent, and hopefully nicer, integral.
The only thing to remember is that you must substitute not only for the
function, but also for the differential. I know this part sounds confusing,
and it is. I wont lie to you: solving integrals analytically is tough, even on
the best of days. Thats why people tend to resort to numerical methods so
often. Of all the ways to get the values of messy integrals, by far the most
effective is a change of variables. Unfortunately, theres no easy way to guess
what sort of change will get the answer. It involves trial and error, a lot of
insight, more than a little luck, and a lot of practice.
Just remember my major Professors words: Conceptually, theres no
problem.
262
9
Chapter 9
263
264
Figure 9.1
Slope =
d
f(x)
dx
y = f(x)
Unfortunately, we live in the real world, and in the real world, Murphys
Law still holds. The sad but true fact of life is that there are many more calculus problems that cant be solved than there are problems that can be
an infinite number, in fact. No matter how thick your book of integrals, it
always seems that the problem you have to solve at the moment isnt in
there. When an integral or derivative cant be found, when that elusive
closed-form solution doesnt exist, what do you do?
Simple: let the computer do it. In other words, do the calculus numerically. A number of useful methods are available to find integrals and derivatives (mostly the former) when the analysis gets too hairy, and these
methods are used every day to solve most of the important math problems
that are solved by computer. The methods are useful in three areas: numerical integration (two types), numerical differentiation (usually for some kind
of search procedure), and numerical interpolation.
265
Aside from missing out on the sheer self-satisfaction of having found an
analytic integral, the downside of numerical methods is that the constants of
the problem are all tangled up in the integral to be found. The numerical
result is just what the name implies: A number, or at best, a set of numbers.
Change any parameter in the equation, and you must redo the calculation to
get a new set of numbers.
The upside is that you can throw away your books of tables, and you
dont have to be a calculus wizard. The two basic principles and some ingenuity are all you need to solve the problem. Computing things such as areas
and slopes should be duck soup for a computer. Unfortunately, Murphy isnt
finished with you yet. Getting a numerical solution is easy enough; getting
an accurate one with limited computing power takes a little longer.
The discipline of performing calculus numerically is called, for reasons
that are lost in the mists of time, Numerical Analysis. Thats what I will talk
about in this chapter. Ill begin with the basic concepts and give you techniques that will work for both the quick and dirty cases and those more
sophisticated cases in which high accuracy is needed. Ill begin by looking at
numerical integration of the first type, called quadrature. Youll see the general approaches and ideas that underlie all of the numerical methods. In the
process, Ill also show you one of my favorite speed secrets, a neat
method for generating integration and interpolation formulas to any desired
order.
Return to Figure 9.1. If the integral (the area under the curve) is taken
between two specific values of x, the result is a single number, the area, and
is called a definite integral. An indefinite integral is another function. To get
it, imagine that the first limit is x = 0 and the second slides to the right from
zero to infinity. As the right border moves, the two limits enclose more area,
and the resulting area can be plotted against x and is considered a new function of x.
It can be shown that the integral and the derivative are inverses of each
other. That is, the derivative of the indefinite integral of (x) is simply (x)
again. And vice versa, except for a possible additive constant.
You have to admit, these concepts are pretty darned straightforward. If
you have any problems with them, just stare at Figure 9.1 until it soaks in.
Generally, I find that its not the basic concepts that stop people in their
tracks, but the notation, which tends to look like so much chicken-scratching when you first see it.
Im afraid theres not much I can do about the notation: Its one thats
been held over for centuries. After a while, though, youll find that it begins
266
[9.1]
A =
f ( x i) x
i=0
x6
[9.2]
A =
f ( x) d x
x0
As you can see, the notation is similar enough to look familiar, but just different enough so the two forms cant be confused.
In a similar way, you can determine the slope of a line by picking two
points on the line, say (x1, y1) and (x2, y2). The slope is defined to be
[9.3]
y
slope = ------ .
x
(It doesnt really matter which two points you choose to find this slope
because the ratio of the two differences is constant for any two distinct
points on the line.)
Again, the notation used for the derivative is a minor modification of
Equation [9.3].
[9.4]
dy
d f ( x)
slope = ----- = ------------dx
dx
First Approximations
Equation [9.1] is a first, and crude, method to approximate the integral. The
integral was based on the notion of splitting the area into rectangular segments, as in Figure 8.6. The math was given in Equation [8.19] and is
repeated here.
N1
[9.5]
(x) dx h
i=0
where, for simplicity of notation, Ive let i represent (xi), and replaced x
by h. (Why h? I dont know. Like so many symbols in math, this one is traditional.) For obvious reasons, the method of Figure 8.6 and Equation [9.1] is
called rectangular integration.
First Approximations
267
dy y
In practice, you need only choose two arbitrary points in the neighborhood
of xi, compute the differences x and y, and divide them. Not surprisingly,
this is called the method of divided differences. In a pinch, you can use xi as
one of the two points; in fact, this method is often practiced. But I think you
can see that youre likely to get a somewhat better approximation if you
choose two points that straddle xi.
If I had unlimited computing power, I could wrap this chapter up right
now. Both of the methods just shown would work fine if I had unlimited
word lengths and plenty of computing time. I have no such luxury in the
real world, so I end up having to compromise accuracy against computing
time.
There are two main sources of error. First, the rectangular approximation is awfully crude. Figure 8.7 shows that the estimate of the integral
didnt really stabilize until Id divided the function into about 500 segments.
Before that, I was underestimating the integral, and one look at Figure 8.6
shows why. As you can see, every segment in the figure leaves out a small
triangular area that should be included, so the estimate is always less than
the actual area under the curve (if the curve sloped downward, the function
would overestimate it). For reasons that will become obvious later, this
error is called truncation error. You can always reduce truncation error by
increasing the number of steps, but that costs computer time.
Even if you could afford the computer time needed to add several hundred, or even a million, numbers, youre not completely out of the woods
because another source of error creeps in at the other end of the spectrum.
This error relates to the finite word length of floating-point arithmetic.
Remember, a floating-point number can only approximate the actual number to a precision that depends on the number of bits in its mantissa. Each
time you add a small number to a larger sum, you lose some of the lower
order bits in the result. The result is rounded to the finite length of the floating-point word. As I add more and more terms, I accumulate these tiny
rounding errors in a random way, and eventually the results become meaningless. This is called round off error. Round off error can be controlled by
reducing the number of steps but that makes the truncation error worse
again. In practice, you always end up walking a tightrope, balancing the two
effects by a judicious choice of the step size h. The optimal step size is the
268
[9.7]
(x) dx h i .
i=1
The difference between Equations [9.5] and [9.7] is subtle: the range of
summation. The first sum leaves out the last data point; the second sum
leaves out the first data point. In this second case, every subarea will tend to
overestimate the actual area. Not surprisingly, this second approximation is
going to be larger than the first and larger than the exact value, but you can
feel pretty confident that the two results will converge if you make N large
enough.
To find out how large N needs to be, Ill try these two methods on a real
problem. Ill use the same integral that I used before.
4
[9.8]
I =
x dx,
2
The results are shown in Figure 9.2. As expected, the original approximation underestimates the integral for small numbers of intervals, whereas the
second approximation overestimates it. Eventually, they converge at around
300 to 500 steps (intervals).
Two things become immediately apparent. First, the method does work.
Although the first few approximations differ quite a bit, both formulas seem
to be converging on an answer around 21.333, or 64/3, as expected. The
second thing that hits you is the large number of steps required. Although
its not immediately apparent at the scale of Figure 9.2, even after approximately 1,000 steps you still have barely three good digits of accuracy.
Extrapolating a bit, it appears that it will take something like 1,000,000,
000 steps to get nine-digit accuracy (except that round-off error would
destroy the results long before that). So, although the concept of rectangular
areas works just dandy for analytical work, where I can happily take the
limit as N approaches infinity, in the real world the method stinks.
First Approximations
Figure 9.2
269
Two approximations.
70
60
Integral f(x)
50
40
30
20
10
0
1.E+00
1.E+01
1.E+02
1.E+03
Number of Intervals
[9.9]
A low h
i=0
and
N
[9.10]
A high h i ,
i=1
1
A = --- ( A high + A low ) .
2
In summing the two terms, note that every i except the first and last are
added twice, thus canceling the factor of 1/2 in the front. Only those two
terms are treated differently. The result can be written
270
[9.12]
0
(x) dx h ----2- +
N1
i + -----2N-
i=1
As you can see, the middle summation is identical to the terms found in
Equations [9.5] and [9.7], except for the range of summation. You can
transform either of these two equations into Equation [9.12] by adding or
subtracting one-half of 0 and 1. Because these formulas are so similar, its
difficult to imagine that the results could be much better, but they are.
How much better? To find out, Ive plotted the error as a function of N
for all three cases in Figure 9.3. The trend is more obvious if I use logarithmic scales. As you can see, thr errors for rectangular integration, as in Equations [9.5] and [9.7], are almost identical.
However, look at the lower curve, which is obtained from Equation
[9.12]. These results are not just better, theyre dramatically better. Not only
does the error start out four times smaller than for the simpler approximations, but it has a slope thats twice as steep. Thats very important, because
it means you can obtain practical accuracies: around one part in 10 8 in a
reasonable number of steps about 32,000.
Figure 9.3
Error behavior.
A Point of Order
271
A Point of Order
The behavior of the error curves in Figure 9.3 certainly indicates there is
something fundamentally different between the first two and the third methods. The reason is that Equation [9.12] represents a higher order approximation than the other two. Its time to explain the term order and put it
on a more rigorous foundation. To do that, I can derive Equation [9.12]
from a different viewpoint.
Instead of approximating the area under the curve by rectangles as I did
in deriving Equations [9.5] and [9.7], Ill use trapezoidal areas, as shown in
Figure 9.4. The area of each trapezoid is
h
A i = --- ( i + i + 1 ) ,
2
Figure 9.4
A better approximation.
This is exactly the same form that led to Equation [9.12], and in fact the
equations are identical. In other words, averaging the two methods for rectangular integration gives exactly the same results as using the trapezoidal
integration of Figure 9.4.
272
[9.13]
(x) dx = h
i + O(h)
i=0
and
[9.14]
0
(x) dx = h ----2- +
N1
i + -----2N-
+ O(h ) .
i=1
The last term in each equation is read order of and implies that the
error term is of Order 1 or 2. Knowledge of the order is important because
it tells how you can expect the error to vary with changing values of the step
size h. In the first-order method, halving the step size halves the error. In the
second-order method, halving the step size reduces the error by a factor of
four. Thats exactly the behavior that Figure 9.3 displays.
Another point here is far more important, and I cant emphasize it
enough. I got the first-order method by fitting a zeroth-order polynomial
(the constant xi) and the second-order method by fitting a first-order polynomial. This is a thread that runs through every one of the methods of
numerical calculus, so burn this into your brain:
The fundamental principle behind all numerical methods is to approximate the function of interest by a series of polynomial segments.
The reason is very simple: we know how to integrate and differentiate
polynomials. We also know how to fit a polynomial through a series of
points. Once you get this concept fixed in your head, the rest, as my major
professor used to say, is merely a matter of implementation.
A Point of Order
273
(x) = ax + bx + c .
I want to integrate this function over the range x0 to x2. But dont forget
that
x0 = x1 h
and
x2 = x1 + h .
I=
(x) dx
x1 h
x1 + h
( ax + bx + c ) dx
x1 h
a 3 b 2
= --- x + --- x + cx
3
2
x1 + h
x1 h
or
[9.16]
a
b
3
3
2
2
I = --- [ ( x 1 + h ) ( x 1 h ) ] + --- [ ( x 1 + h ) ( x 1 h ) ]
3
2
+ c [ ( x 1 + h ) ( x 1 h ) ].
274
( x1 + h ) ( x1 h )
3
= ( x 1 + 3x 1 h + 3x 1 h + h ) ( x 1 3x 1 h + 3x 1 h h )
2
= 6x 1 h + 2h .
Similarly,
2
( x1 + h ) ( x1 h )
2
2
2
= ( x 1 + 2x 1 h + h ) ( x 1 2x 1 h + h )
= 4x 1 h
and
( x 1 + h ) ( x 1 h ) = 2h ,
then
[9.17]
a
b
2
3
I = --- [ 6x 1 h + 2h ] + --- [ 4x 1 h ] + c [ 2h ]
3
2
h
2
2
= --- [ a ( 6x 1 + 2h ) + 6bx 1 + 6c ].
3
h
2
2
I = --- [ a ( x 1 h ) + b ( x 1 h ) + c + 4ax 1 + 4bx 1
3
2
+ 4c + a ( x 1 + h ) + b ( x 1 + h ) + c ].
(The easiest way to prove this is to expand Equation [9.18]. Youll find that
most of the terms cancel, leaving you with Equation [9.17].)
Look at the nine terms in Equation [9.18] and compare them to Equation [9.15]. You can see that the first three are simply (x0), or 0. Similarly,
the next three are 41, and the last three 2, so I end up with
[9.19]
h
I = --- ( 0 + 4 1 + 2 ) .
3
Now its finally starting to look usable. To complete the job, I need to
add up all the areas for each set of three points. Remember, this area I spans
two intervals: that from x0 to x1 and that from x1 to x2. I must repeat the
A Point of Order
275
process and add up all the areas for every pair of intervals in the desired
range. Note that this requires that N be an even number. In the general case,
[9.20]
h
(x) dx = --3-
n2
( i + 4 i + 1 + i + 2 ) + O(h ) .
i = 0
n even
This is Simpsons Rule. Its one of the most popular methods for this kind
of integration because it represents a nice balance between accuracy and
simplicity. Because I began with a second-order formula, its reasonable to
expect a third-order integral. Simpsons rule is, however, fourth order.
Because its a fourth-order method, I can expect the error to go down by a
factor of sixteen each time I cut the step size in half.
I can continue in the same vein to get formulas of even higher order.
How high should the order be? In general, the higher, the better. All other
things being equal, a higher order method requires fewer steps to get the
same accuracy. Because it requires fewer steps, the higher order method is
less likely to cause round-off error. On the downside, the polynomial fits
tend to become unstable as the order goes higher. Imagine that I have 10
points as in Figure 9.5. Chances are that the curve they represent is smooth,
like the dashed curve. But if I try to fit a ninth-order polynomial through
these points, theres a good chance Ill end up with the crooked curve
shown, with a lot of extraneous wiggles in it. This effect is caused by numerical errors in the point values. Even though the upper and lower lobes of the
wiggly polynomial ought to pretty much cancel each other out, you just
have to believe that some error is going to creep in because of the wiggles,
and indeed it does. Because of this, theres a practical limit to the orders you
can use. Ive seen integrations done with orders as high as 13, but most
work gets done with orders of three through seven. Ill come back to this
point and to higher order integration later. But first, Id like to take a look at
the other end of the spectrum.
276
Figure 9.5
Differentiation
Finding the derivative of a function is called differentiation. (Why isnt it
derivation? Again, who knows?) Numerical differentiation is not used
nearly as often as numerical integration, mainly because its not needed
nearly as often. Differentiating a function analytically is far, far easier than
integrating one, so you can usually find the derivative you need without
having to resort to numerical methods. Furthermore, numerical differentiation is notoriously inaccurate and unstable, so programmers tend to use it
only when there is no alternative. Another look at Figure 9.5 should convince you that if integration using higher orders can be unstable, differentiation, which involves the slope of the fitted polynomial, can be disastrous.
Still, many problems exist for which numerical differentiation is the only
practical approach, so its important to have the methods in your bag of
tricks.
If you followed my advice and burned the main principle into your brain,
you already know the general approach to use: approximate the given function by segments of polynomials and then take the derivative of the polynomials. The first-order approximation is easy. I simply fit a straight line
between two adjacent points (as in the trapezoidal integration of Figure
9.4).
A Point of Order
[9.21]
277
1
y
dy
----- = ------ = --- ( i + 1 i ) .
h
dx
x
The next problem is which value of i to use. The obvious but somewhat useless answer is to use the one that gives the best approximation to the actual
derivative.
You can really have two kinds of problems, each of which requires a
somewhat different approach. First, suppose that I have a computer function that can evaluate (x) for any x. In this case, it makes sense to let i be
represented by (x). I can replace i + 1 by (x + h), and since Im not talking
about tabular points at the moment, I can let h be any value thats convenient. Thus,
[9.22]
dy 1
----- --- [ (x + h) (x) ] .
dx h
Choosing a good value for h is even trickier for this problem than for
integration, but Ill follow the same informal idea: start with a fairly large
value of h and reduce it until the result seems to be good enough. The
same rules about errors apply. You can go too far in reducing h, because as
h decreases, (x + h) becomes almost identical to (x), so you end up subtracting two nearly equal numbers. As anyone whos ever worked much
with floating-point numbers can tell you, this is a sure way to lose accuracy.
Its the same old trade-off: truncation error if h is too large, round-off error
if its too small. The difference is that, because the terms are subtracted in
Equation [9.22], rather than added as in the integration formulas, the
round-off error becomes much more of a serious problem, and thats why
numerical differentiation has a deservedly bad reputation.
Theres no law that says that I must choose the second point as the one to
the right of x. As I did with the first-order integration, I could just as easily
choose a different approximation for the derivative by using a point to the
left of x. This leads to
[9.23]
dy 1
----- --- [ (x) (x h) ] .
dx h
As I did in Figure 9.2, I can compute the derivatives using both methods.
Comparing the two results gives me more confidence that Im on the right
track.
278
dy 1
----- ------ [ (x + h) (x h) ] .
dx 2h
(x + h) = a ( x + h ) + b ( x + h ) + c
2
= a ( x + 2hx + h ) + b ( x + h ) + c.
Similarly,
(x h) = a ( x 2hx + h ) + b ( x h ) + c .
When I subtract these two expressions, most of the terms cancel to give
4ahx + 2bh.
dy
2
----- = 2ax + b + O(h ) .
dx
Tabular Points
279
for
x = 1/2.
Tabular Points
While finding the derivative of a continuous function is possible, as Ive
described it above, you rarely need to actually do it that way since, given a
mathematical formula for a function, you can almost always find the derivative analytically. A much more likely situation is the case in which youre
given not an analytical function, but a table of values at discrete points xi.
The task is to estimate a corresponding table of derivatives. For this case, I
can go back to the form of Equation [9.21] and write approximations
equivalent to Equations [9.22] through [9.24].
[9.26]
1
dy
----- = --- ( i + 1 i ) + O(h)
h
dx
[9.27]
1
dy
----- = --- ( i i 1 ) + O(h)
h
dx
[9.28]
1
dy
2
----- = ------ ( i + 1 i 1 ) + O(h )
2h
dx
280
Figure 9.6
Interpolation/extrapolation.
The slope of the line can be computed from any two pairs of points. I can
write
1 0
(x)
slope = --------------- = --------------------0- .
x1 x0
x x0
281
1 0
(x) = 0 + ( x x 0 ) ---------------- .
x 1 x 0
This formula is exact when the curve is a straight line, as in Figure 9.6. If
the curve is not a straight line, its still a good formula as a first-order (linear) approximation. This is the formula for linear interpolation, which is
the kind most often used. Note that if the function is truly a straight line, it
makes no difference whether x lies between x0 and x1 or not. If it does not,
then youre really doing extrapolation rather than interpolation, but the
principle, and even the formula, is the same. Of course, if the curve is not
actually a straight line, then the formula is only approximate, and it stands
to reason that the farther x is from the range between x0 and x1, the worse
the approximation is going to be. Thats life when it comes to extrapolations.
It is possible to derive higher order interpolation formulas. The method
is the same as usual: fit polynomial segments to the tabular points then evaluate the polynomial at the point of interest. I wont do that now because I
have other matters to discuss that are far more important. After you learn
these additional techniques, the interpolation formulas will be much easier
to understand and implement.
d
(x) = 0 + ( x x 0 ) ------ .
d x 0
The subscript zero on the derivative is used to indicate that the derivative
should be evaluated at x = x0. As long as I assume the curve is a straight
line, it really doesnt matter where the slope is evaluated, but Im not going
to maintain that assumption any longer.
282
[9.31]
2
d
1
2 d
(x) = 0 + ( x x 0 ) ------ + ----- ( x x 0 ) --------2 +
dx 0 2!
dx
0
n
1
n d
+ ----- ( x x 0 ) --------n + .
n!
dx
0
This is the Taylor series. Its an infinite series with which you can extrapolate any function from x0 to any other point x, providing you know not
only the value of (x) at x = x0, but also every one of its derivatives, all evaluated at x = x0. So much for interpolation and extrapolation; this formula
covers them all. There is that small matter of knowing all those higher order
derivatives, which might seem at first to be insurmountable. But what the
Taylor series lets you do is to separate the problem into two separate parts:
find the derivatives however you can, then use the series to interpolate or
extrapolate. Often, you can find the derivatives by using the Taylor series
backwards. Ill show you how in a moment, but first I need to build a
base to work from.
x i = x i + 1 x i .
x i = x i x i 1 .
283
Ive been very specific about the definitions by attaching an index to the
difference as well as to the individual values. As the name implies, the forward difference is computed from the current value and the next (forward)
value in the table. Similarly, the backward difference uses the previous value
and the current value. These two differences are not independent, since
[9.34]
x i = x i + 1 .
d i
1
------- = --- i + O(h)
h
dx
[9.36]
d i
1
------- = --- i + O(h)
h
dx
1
x i = --- ( x i + x i ) ,
2
d i
1
2
------- = --- i + O(h ) .
h
dx
1
x i = --- ( x i + 1 x i 1 ) .
2
284
Big-Time Operators
At this point, you may be wondering where Im going with all this and wishing I werent getting so bogged down in definitions. But, believe it or not,
Im right on the verge of some concepts that can crack this case wide open.
Take another look at Equation [9.37]. As you can see, every value of x
in the equation is evaluated at the point xi. In a way, I can simply think of
the differences as multiplying xi. More precisely, I must think of them as
operators that operate upon something; in this case, xi. Acting very
naively, I could factor xi out, leaving the relation
[9.40]
1
= --- ( + ) .
2
At first glance, an equation like this seems strange. Ive got differences,
but theyre not differences of anything. The deltas are pure operators. In
practice, they have to operate on something, either xi, i, or whatever. Equation [9.40] really only makes sense if I apply these operators to a tabular
value and perform the arithmetic that the definitions of the operators imply.
But it turns out that, in many situations, I can just blithely write equations
such as Equation [9.40], pretending that these are merely algebraic symbols,
and manipulate them as such. Happily, I always get the right answer. This
concept of operator notation is incredibly powerful and is the key to determining higher order formulas. I can perform complex algebraic manipulations on the operators, without worrying about what theyre going to
operate on, and everything always comes out okay in the end.
Using the same technique, I can define a relationship between the operators and . Consider that
x 1 x 0 = x 0 = x 1 .
Again, if I assume that I can factor out the x0, treating as simply an
algebraic quantity, I get
[9.41]
x 1 = ( 1 + )x 0 .
x1 ( 1 ) = x0 .
285
1
1 + = ------------- .
1
From this somewhat remarkable relation, I can solve for either difference in
terms of the other:
[9.44]
[9.45]
= ------------- 1 = ------------1
1
or
= ------------1+
Now I have a neat way to convert between forward and backward differences. But theres much, much more to come. Recall that I started this little
excursion into operators using Equation [9.40], which gave in terms of
and . Now I can use it with Equations [9.44] and [9.45], to get
1
= --- ( + )
2
1
= --- + -------------
2
1 +
1 (1 + ) +
= --- -------------------------------2
1+
1 (2 + )
= --- --------------------2 1+
or
[9.46]
(2 + )
= --------------------- .
2(1 + )
Similarly,
[9.47]
(2 )
= ---------------------- .
2(1 )
286
= -------- .
2
= + -------- .
2
-------- = + -------2
2
or
[9.50]
= .
Can this be possible? Can the difference of the two operators possibly be the
same thing as their product? This hardly seems reasonable, but amazingly
enough, thats exactly what it means. If you dont believe it, you can easily
prove it to yourself by performing each operation on xi. You can also easily
prove that
[9.51]
= .
The z-Transform
The z-transform is the foundation of all digital signal processing, and learning how to use it is the subject of whole courses or curricula. You may not
have noticed, however, that it fell out of my analysis. That it did so only
serves to illustrate the power of operator algebra.
Take another look at Equation [9.41],
x 1 = ( 1 + )x 0 ,
The z-Transform
287
Before, I used these two equations to get a relationship between the forward
and backward difference operators. But I can look at them another way. In
either equation, I can consider the term in parentheses as a new operator.
Let
[9.52]
1
z = 1 + = ------------- .
1
x 1 = zx 0 .
As you can see, z is a shift operator; it advances the index by one. Here it
changes x0 to x1, but Im sure you can see that it will work for any index.
Again, if I pretend that z is really an algebraic quantity instead of an operator, I can blithely revert Equation [9.53] to
[9.54]
x0 = z x1 .
288
d
d
D(x) = ----- (x) = ------ .
dx
dx
[9.56]
h D
h D
1 = 1 + hD + ------------ + + ------------ + 0 .
n!
2!
x1 = e
hD
x0 .
The z-Transform
289
Surely, this time Ive gone too far. Treating operators like algebraic variables is one thing, but how do I raise Eulers constant, e, to the power of an
operator? Easy I just expand the exponential back to its series again. You
can think of Equation [9.57] as simply a shorthand form of Equation [9.56].
But incredible as it may seem, I can continue to treat this exponential as an
operator. It always works.
Theres more. Comparing Equations [9.53] and [9.57], you can see that
[9.58]
z = e
hD
This remarkable relationship is the Rosetta stone that connects the world of
continuous time that we live in with discrete time in the world of computers
and digital signal processing. Using this relationship, plus the others Ive
already derived, you can express all the other operators in terms of D and
vice versa. A complete set of these relations is shown in Figure 9.7. Some of
these relations are going to seem totally screwball to you, but trust me: they
work. Remember, all of the functions shown have series expansions.
Figure 9.7
Operator relationships.
= ------------1
= z1
=e
=e
hD
sinh1
= ------------1+
= 1 z 1
= 1 e hD
= 1e
sinh1
(continued)
290
1
= --- ( + )
2
(2 + )
= --------------------2(1 + )
(2 )
= ---------------------2(1 )
2
z 1
= ------------2z
z = 1+
1
= ------------1
=e
=e
hD
sinh1
1
D = --- ln ( 1 + )
h
1
= --- ln ( 1 )
h
1
= --- ln z
h
1
= --- sinh1
h
10
Chapter 10
Putting Numerical
Calculus to Work
Whats It All About?
In the last chapter, I began with the fundamental concepts of calculus and
defined the terms, notation, and methods associated with the calculus operations called the integral and derivative of continuous functions. In looking
at ways to explain these operations and in thinking about how to implement
them in a digital computer, several other operators naturally popped up.
These were the forward, backward, and central difference operators, as well
as the shift operator, z.
[10.1]
x i = x i + 1 x i
forward difference
[10.2]
x i = x i x i 1
backward difference
[10.3]
1
x i = --- ( x i + x i )
2
central difference
[10.4]
zx i = x i + 1
shift operator
291
292
z = e
hD
where h is the step size, or time interval between successive points, and D is
the ordinary derivative operator with respect to time.
[10.6]
D =
d
dt
Differentiation
[10.7]
293
V = V(t),
Differentiation
The most straightforward use of Equation [10.5] is the most handy: compute the derivative of a function. Remember, the secret to using these operators is to forget that theyre operators and naively manipulate them as
though they were simple algebraic variables. If you do that and revert Equation [10.5], you get
[10.9]
hD = ln z.
But what does this really mean? How can you take the logarithm of an
operator? Simple: as in Equation [10.5], the formula is meaningless until
you expand the function back into a series. To do that, recall the relationship between z and given in Figure 9.7:
[10.10]
z = 1 + .
hD = ln(1 + ).
294
x
x x
ln ( 1 + x ) = x ----- + ----- ----- + .
2 3 4
[10.12]
hD = ------ + ------ ------ + .
2
3
4
At this point, youre probably thinking thats clear as mud. How do you
use it? The equation above doesnt seem to be much help, but again, the
answer is right there, almost shouting at you. An example should make
everything clear. The first step is to use those operators to operate on something.
The Example
Using Equation [10.12] to operate on fi gives
2
[10.13]
f f f
1
D f i = --- f i -----------i + -----------i -----------i + .
h
2
3
4
The first difference is defined in Equation [10.1]. The higher order differences are simply differences of differences; that is,
2
f i = ( f i ),
3
f i = ( f i ) = ( ( f i ) i ),
and so on. You can compute all these differences from the table of the function itself. Try it with the function
[10.14]
(x) = x2 3 sin(x),
Differentiation
295
f(x)
What is the derivative of (x) when x = 2? To find out, you can build a
table of the values and their differences in that vicinity (Table 10.1).
Table 10.1
Difference table.
f(x)
2f
3f
4f
2
2.01
2.02
2.03
2.04
5.27210772
5.40532931
5.540028425
5.676209777
5.813878047
0.133221591
0.134699115
0.136181351
0.137668271
0.001477525
0.001482236
0.001486919
4.7108e06
4.68378e06
2.70233e08
Start with just the values of x and (x). Since, from Equation [10.1],
i = i+1 i,
all you have to do to fill the next column is subtract the current value of f(x)
from the next one in the table. Put another way, each difference is given by
simply subtracting two values from the column to its left: the entry directly
to the left, and the one below it. If you want to generate a table of derivatives, do this for every row (except the last few, for which there are no
296
Table 10.2
Estimated derivatives.
Order
Derivative
1
2
3
4
13.32215905
13.2482828
13.24843983
13.24844051
Exact
13.24844051
Now that youve seen this example, I hope you can see how the seemingly esoteric formulas of Figure 9.7 really do obtain practical results.
Whats more, the translation from the operator notation to a practical
implementation is straightforward. Whatever the problem at hand, manipulate the equation to express the thing youre looking for (in this case, D),
expand the resulting formula into a power series of differences, then build a
table of the differences to get the final result. It is simple and practical. The
nice part about Equation [10.13] is that you can see immediately how to
extend the solution to any desired order of approximation. You should also
see that this extension doesnt take much extra work or extra CPU time.
Backward Differences
297
Backward Differences
In Equation [10.13] and the above example, note that I chose to use forward differences. I could just as easily have used backward differences. Substituting for z in Equation [10.9], I could have written
[10.15]
1
hD = ln ------------- = ln ( 1 ) .
1
The logarithm series is exactly the same as before, except you replace
by . The result is the backward equivalent of Equation [10.12]:
2
[10.16]
hD = + ------ + ------ + ------ + .
2
3
4
[10.17]
f f f
1
D f i = --- f i + -----------i + -----------i + -----------i + ,
h
4
3
2
which, as you can see, is virtually identical to Equation [10.13] except for
the signs and the backward differences.
Given a choice between Equations [10.13] and [10.17], you should probably use Equation [10.13], because theoretically, the alternating signs
should result in faster convergence (i.e., fewer terms are required). However,
remember that Equation [10.17] has the great advantage that it requires
only backward differences, which makes it useful when the data is streaming in in real time. More to the point, in practice, I find no appreciable difference in accuracy. The results from applying the backward difference
formula to the example problem match Table 10.2 so closely that I wont
even bore you by repeating it. However, I do think its important to show
the difference table so you can see the structural differences.
As you can see in Table 10.3 and as the name implies, the backward difference table always looks to the past, not the future, so you need only those
entries in the table that help to build the bottom row. Contrast this with
Table 10.1, where the top row was used in the solution.
298
Table 10.3
Backward differences.
f(x)
2f
3f
4f
1.96
1.97
1.98
1.99
2
4.753901438
4.881260581
5.010078134
5.140358916
5.27210772
0.128817553
0.130280782
0.131748804
0.133221591
0.001468022
0.001472787
0.001477525
4.73795e06
4.7108e06
2.70233e08
Seeking Balance
Youve probably noticed that whichever formula you choose, you end up
with a one-sided result. Equation [10.13] uses only the current and later values of x, whereas Equation [10.17] uses only the current and earlier values.
Its reasonable to suppose that youd get an even better approximation if
you used values on both sides of the desired x. I suppose you could use a
combination of forward and backward differences, but this is clearly the
kind of problem that begs for the balanced arrangement that central differences offer.
The material in this chapter was first published in Embedded Systems
Programming (Crenshaw 1993). In that article, I tried to develop a formula
using central differences in a very simple way using a technique Ive found
helpful in the past: average the results of two similar algorithms. I wrote a
formula derived by averaging Equations [10.13] and [10.17]. The results
were mixed for two reasons. First, I made a really dumb sign error in my
expression for the central difference that led into a blind alley of ridiculous
statements and wrong conclusions. Second, and more important, averaging
the two equations turns out to give a result that, while not exactly wrong, is
not optimally correct, either. I expected the averaged equations to simplify
down to a set of terms involving only central differences, but that didnt
happen.
Even though I didnt feel right about what Id written, under the pressure
of a tight deadline I didnt have the time to fix it. Later, I published an errata
straightening out the definition for the central difference, but I never revisited the balanced equation for the derivative.
Fortunately, Ive learned some things since 1993, from which you will
benefit. Fixing the silly sign error changed the relationships in Figure 9.7
for the better, so this time around, I can give you a formula involving only
Seeking Balance
299
central differences. The results are astonishing; I think you will be amazed
at how well the formula works.
I also understand now why the averaging didnt work. Although its
mostly only of historical interest, Ill briefly discuss the differences between
the two approaches.
As it turns out, I was making the problem much more difficult than it
needed to be. The key to the central difference formula is found in Figure
9.7:
[10.18]
1
1
D = --- sinh .
h
To get the central difference formula, I need only expand the power
series. My trusty table of integrals (Pierce 1929), which also has series
expansions, tells me that the series for sinh1(x) is
3
1x 1 3x 1 3 5x 1 3 5 7x
1
sinh ( x ) = x --- ----- + ---------- ----- ------------------ ----- + ------------------------- ----- .
23 245 2467 24689
[10.19]
1
1
13
135
1357
D = --- --- ----- + ---------- ----- ------------------ ----- + ------------------------- ----- .
h
2 3 2 4 5 2 4 6 7 2 4 6 8 9
The beautiful aspect of this equation is that, as you can see, it involves
only odd orders of the differences. This means that, to get the same order of
accuracy, youll need only about half as many terms in your approximation
as you would with either the forward or backward difference formulas.
Note the pattern of the coefficients: the product of odd integers in the
numerator and even integers in the denominator and a term divided by the
power of the next odd integer. Once you see the pattern, you can extend this
formula to any order desired. However, as you will see, if you need more
than Order 9, somethings seriously wrong.
Table 10.4 shows the differences for the sample problem.
300
Table 10.4
Central differences.
f(x)
2f
1.95
1.96
1.97
1.98
1.99
2
2.01
2.02
2.03
2.04
2.05
4.627995855
4.753901438
4.881260581
5.010078134
5.140358916
5.27210772
5.40532931
5.540028425
5.676209777
5.813878047
5.953037894
0.126632363
0.128088348
0.129549167
0.131014793
0.132485197
0.133960353
0.135440233
0.136924811
0.138414059
0.001458402
0.001463222
0.001468015
0.00147278
0.001477518
0.001482229
0.001486913
3f
4f
5f
1.95
1.96
1.97
1.98
1.99
2
2.01
2.02
2.03
2.04
2.05
4.80642e06
4.77896e06
4.75162e06
4.72441e06
4.69732e06
2.74006e08
2.72771e08
2.71509e08
1.24833e10
Remember, each central difference entry is based on one point each side
of the current point (ahead and behind). Note that I had to extend the difference table to one more order because the fourth-order difference isnt
used. The results of the computation are shown below in Table 10.5.
Seeking Balance
Table 10.5
301
Derivative
1
3
5
13.2485197
13.24844051
13.24844051
Exact
13.24844051
As you can see, I didnt need the fifth-order approximation (and therefore the fourth difference); the estimation was already dead-on by the time I
got to third order. This is a rather remarkable result because I get a highaccuracy approximation to the derivative with only two terms in the formula:
3
[10.20]
f
1
D = --- f i ----------i .
h
6
x
x x
ln ( 1 + x ) = x ----- + ----- ----- + .
2 3 4
1
1+
D = ------ ln ------------- .
2h 1
302
[10.22]
1+x
x
x
ln ------------ = 2 x + ----- + ----- +
1 x
3 5
Getting Some zs
The main advantage of the series formulas derived above in terms of the differences is the very fact that they are extensible. You can see the pattern in
the series and thus extend them as far as you like. This is especially useful in
hand computations, because you can see the values of the differences and
thereby tell when its pointless to add more terms. But some people prefer to
compute things directly from the values of i, without having to compute
the differences. No storage is saved by doing this its either store past (or
future) values of i or store the differences but fewer additions or subtractions are required. The disadvantage is that you lose the extensibility;
the results are completely different depending on where you truncate the
series.
For those who prefer to work only with function values, you could begin
with the equation directly relating D and z in Figure 9.7. However, this is no
good because the power series for the logarithm involves the argument (1 +
x), not x. Thus, youre better off starting with Equation [10.16].
Getting Some zs
303
hD = + ------ + ------ + ------ +
2
3
4
1 3
1 4
1
(1 z ) (1 z ) (1 z )
1
D = --- ( 1 z ) + ----------------------- + ----------------------- + ----------------------- +
h
2
3
4
This formula, like the others, is extensible to any order with a difference: you cant get a formula thats explicitly in powers of z unless you
expand each polynomial in the numerators. First, decide how far out to take
the approximation, then expand the polynomials and collect terms. The
process is tedious but perfectly straightforward. A symbolic algebra program like Maple, Mathcad, Mathematica, or Matlab will generate the formulas with ease.
In Figure 10.2, Ive shown the resulting approximations for Orders 1
through 10, which ought to be enough for any problem. The coefficients for
the higher order formulas look messy, but the algorithms themselves are
quite simple. Computationally, if youre using the z-operator formulation,
you dont need to compute columns of differences, so you only need to
maintain enough storage to keep the necessary past values.
Order 2
1
1
2
D = ------ ( 3 4z + z )
2h
304
Order 4
1
1
2
3
4
D = --------- ( 25 48z + 36z 16z + 3z )
12h
Order 5
1
1
2
3
4
5
D = --------- ( 137 300z + 300z 200z + 75z 12z )
60h
Order 6
1
1
2
3
4
5
6
D = --------- ( 147 360z + 450z 400z + 225z 72z + 10z )
60h
Order 7
1
1
2
3
4
5
6
7
D = ------------ ( 1089 2940z + 4410z 4900z + 3675z 1764z + 490z 60z )
420h
Order 8
1
1
2
3
4
5
6
D = ------------ ( 2283 6720z + 11760z 15680z + 14700z 9408z + 3920z
840h
960z
+ 105z )
Order 9
1
1
2
3
4
5
D = --------------- ( 7129 22680z + 45360z 70560z + 79380z 63504z
2520h
+ 35280z
12960z
+ 2835z
280z )
Order 10
1
1
2
3
4
5
D = --------------- ( 7381 25200z + 56700 100800z + 132300z 127008z
2520h
+ 88200z
43200
+ 14175z
2800z
+ 252z
10
How well does the z operator version work? In Table 10.6, I show the
results of the derivative of the sample function for each order.
Numerical Integration
Table 10.6
305
Derivative
1
2
3
4
13.17488035
13.24828144
13.2484412
13.24844051
Exact
13.24844051
Although I dont show the table for the backward differences, you
should know that the results obtained here for the z operator are identical
to those obtained using backward differences. This shouldnt surprise you
because both the z operator and the backward difference operator use
exactly the same data, the tabular values of f(x) from x = 1.96 through x =
2.0.
Numerical Integration
Quadrature
Now that youve seen the general method for applying the Rosetta Stone
formula, you should also see that it can be used for virtually any problem
involving tabular data. About the only limitation, other than imagination, is
the restriction that the data be tabulated with a fixed interval, h.
There are two kinds of numerical integration. In the first and simplest
kind, called quadrature, you are given a function (x) or a table of its values
and are asked to find the area under the curve. Ive already covered this
case, so Ill limit the discussion here to a review and clarification of relationships between theoretical concepts, reality, and numerical approximations
to it.
In Chapter 8, I defined the integral of a function (x) as the area under
the curve. Then I discussed how to go about approximating the function by
a series of rectangular areas (see Figure 8.6 ). I used that model as a springboard into the mathematical concept of an integral, where a summation
approximating the area under the curve is converted to an integral exactly
equal to that area as the number of rectangles increase without limit.
In Chapter 9, I figuratively turned the telescope around and looked into
the other end. Instead of using the concept of adding rectangular areas as a
306
An Example
To bend an old homily, an example (with pictures) is worth a thousand
words. To see numerical quadrature in action, Ill consider the integral of
the following function, which is similar to the function used in the discussion of slopes.
3
[10.24]
x
f (x) = sin ( x ) + -----40
Numerical Integration
[10.25]
f ( x) d x
307
h
= --- [ ( f 0 + 4 f 1 + f 2 ) + ( f 2 + 4 f 3 + f 4 ) ]
3
h
= --- ( f 0 + 4 f 1 + 2 f 2 + 4 f 3 + f 4 ).
3
308
Table 10.7
Integral f(x)
Error
5.10205358479391
4.63446383999906
4.62322450680946
4.62262621328102
4.62259019350767
4.62258796289825
4.62258782380431
4.62258781511592
4.62258781457297
4.62258781453904
4.62258781453691
4.62258781453678
0.4794657
0.0118760
0.0006366
3.8398e5
2.3789e6
1.4836e7
9.2675e9
5.7914e10
3.6193e11
2.2622e12
1.3944e13
7.9936e15
Numerical Integration
309
Note that I get acceptable accuracy with as few as 32 steps and more
than enough accuracy for most purposes with 256 steps. Beyond 4,096
steps, I run into the limits of the Intel math coprocessor. A loglog graph of
these errors, shown in Figure 10.5, displays the telltale slope of a fourthorder approximation: every crossing of a grid line on the x-axis corresponds
to four grid lines on the y-axis.
Error
Number of steps
In the world of numerical integration, the fourth order is one that commands a high degree of respect. The most popular general-purpose integration formula is the fourth-order RungeKutta formula, which Ill discuss
later. Except for things like interplanetary trajectory generators, people
rarely need to go much higher in order. In fact, the fourth-order Runge
Kutta method reduces to Simpsons Rule when applied to a problem of
quadrature. Therefore, Ill accept Simpsons Rule here as a perfectly adequate solution to that problem and wont seek further improvements.
Implementation Details
Before leaving the subject of quadrature, I should say something about both
the structure of a practical integrator and the relationship between low- and
high-order implementations. Consider Equation [9.5] again.
310
N1
f ( x) d x
= h
fi
i=0
This equation implements the integrator derived from the concept of rectangular areas.
Despite the elegance and brevity of summation signs, sometimes I find it
helpful to write out the summation in its long form. To be specific and still
keep the notation short enough, suppose I have only 10 steps in the function
(and therefore 11 tabular values, including beginning and ending values).
Equation 9.5 becomes
[10.26]
I 1a = h ( y 0 + y 1 + y 2 + y 3 + y 4 + y 5 + y 6 + y 7 + y 8 + y 9 ).
Recall that this formula is based on the assumption that the heights of
the rectangles are set at the beginning value of each interval. Alternatively,
when I set it at the ending value, I get
[10.27]
I 1b = h ( y 1 + y 2 + y 3 + y 4 + y 5 + y 6 + y 7 + y 8 + y 9 + y 10 ).
y
y 10
= h ----0- + y 1 + y 2 + y 3 + y 4 + y 5 + y 6 + y 7 + y 8 + y 9 + -----2
2
The second-order formula looks almost exactly like either of the first,
except that the first and last points require a (trivially) different treatment.
This is an encouraging sign that increasing the order will not necessarily
increase the workload appreciably, at least for quadrature.
Look again at Simpsons Rule, which was given in Equation [9.20].
h
f (x) dx = --3-
n2
i = 0
n even
( f i + 4 f i + 1 + f i + 2 ) + O(h )
Numerical Integration
311
Recall that Simpsons Rule requires that you take segments in pairs, involving three points per step. Although the equation above is the correct formula for Simpsons Rule, a direct implementation of it would be terrible.
Thats because the end of one pair of segments is the beginning of the next.
Implementing Equation [9.20] directly would require you to process half the
points twice. This becomes obvious when I write the summation out in its
long form.
h
I = --- [ ( f 0 + 4 f 1 + f 2 ) + ( f 2 + 4 f 3 + f 4 ) + ( f 4 + 4 f 5 + f 6 )
3
+ ( f 6 + 4 f 7 + f 8 ) + ( f 8 + 4 f 9 + f 10 ) ]
h
I = --- ( f 0 + 4 f 1 + 2 f 2 + 4 f 3 + 2 f 4 + 4 f 5
3
+ 2 f 6 + 4 f 7 + 2 f 8 + 4 f 9 + f 10 ).
This is the optimal form for implementation. If I group the terms in pairs, I
can revert back to a summation notation, adding all the interior points then
tweaking the two end points separately. The resulting equation is
[10.30]
h
I = --3
N
---- 1
2
( 2 f 2i + 4 f 2i + 1 ) f 0 + f N .
i=0
312
y = f(x)
*
* Between the limits x1 and x2, dividing this range up into n
intervals.
*
* The nature of Simpson's rule requires that n be even.
*/
double simpson(double (*f)(double), double x1, double x2, int n){
if((N & 1) != 0){
cout << "Simpson: n must be an even integer\n";
return 0;
}
double sum = 0;
double h = (x2-x1)/n;
double x = x1;
for(int i=0; i<n/2; i++){
sum += f(x)+2*f(x+h);
x += 2*h;
}
sum = 2*sum-f(x1)+f(x2);
return h*sum/3;
}
Numerical Integration
313
Trajectories
The second class of problems in numerical integration present considerably
more challenge. I call the problems trajectory generators, for two reasons.
First, I encountered the problem while working at NASA, helping to generate the trajectories of spacecraft. Second, even if the problem doesnt involve
trajectories in the traditional sense of the word it does involve a solution
that depends on initial conditions, and the function to be integrated cannot
be drawn out in advance.
In problems of quadrature, youre given some function f(x), which is forever fixed. Whatever the function, it depends only on x; therefore, you can
draw it on a graph, look at it, analyze it, and decide how best to integrate it.
The integral is, as Ive said so many times, simply the area under the curve
of f(x).
Trajectories are a different animal altogether. In problems of this type,
you are not given the function to integrate but, rather, a differential equation that defines it. In this second case, you begin with an ordinary differential equation of the form
[10.31]
dy
----- = f (x, y) .
dx
Note that the derivative function now depends on y as well as x, which presents a dilemma. You cant simply plot the curve because you dont know
the value of the next y until you know (x, y), which in turn depends on y
again. The best you can do is begin with some initial condition y = y0 and
inch your way forward one small step at a time until the entire table of yi is
constructed.
In a very real sense, the two kinds of numerical integration are completely analogous to the two kinds of integrals you can obtain analytically.
Quadrature is equivalent to the definite integral
x=X
[10.32]
y =
f ( x) d x ,
x=0
which produces a simple, scalar number: the area under the curve of (x).
The second type of integration is equivalent to the indefinite integral
[10.33]
y(x) =
f (x, y) dx .
The result of this kind of integral is not a single number, but a new function: the history of y as a function of x. Because is a function of y as well
314
= P()hD,
yi+1 = yi + hP()i.
Numerical Integration
[10.36]
315
= z.
Because I also need a D on the right-hand side, Ill multiply and divide by D:
D
= z ---- .
D
z
= -------- hD .
ln z
= ----------------------------------------- hD .
( 1 )ln ( 1 )
Now I have the formula in terms of the parameters I need. I still need to turn
the expression in parentheses into a power series in . Because the math gets
quite tedious, Ill merely outline the process here. Recall that the series for the
logarithm gives
2
ln ( 1 ) = + ------ + ------ + ------ + .
2
3
4
( 1 )ln ( 1 ) = + ------ + ------ + ------ +
2
3
4
3
2
+ + ------ + ------ + ------ + .
2
3
4
[10.39]
( 1 )ln ( 1 ) = ------ ------ ------ .
2
6 12
To get the final form, I must now divide this into using synthetic division. The complete process is outlined in Figure 10.6. From this, I get the
first few terms of the integration formula:
[10.40]
5 2 3 3
= 1 + ---- + ------ + --- + hD .
8
2 12
316
5 2 3 3
y i + 1 = y i + 1 + ---- + ------ + --- + h f i .
8
2 12
Because of the synthetic division, things get messy rather fast computing
the higher order terms. Fortunately, a symbolic algebra program like Maple,
Mathematica, or Mathcad can come to the rescue. Using these, Ive computed coefficients through Order 25, which is far more than anyone should
ever need. The formula through Order 7 is shown in Figure 10.7. This
should be more than ample for most of your needs.
8
720
288
2 12
19, 087 6 5, 257 7
+ ------------------ + --------------- + hD
60480
17280
317
Moulton corrector
2
19 4
3 5
= 1 ---- ------ ------ --------- ---------
720
160
2 12 24
863 6
275 7
--------------- --------------- hD
60480
24192
which gives
[10.42]
y i + 1 = y i + Q ( )h f i + 1 .
In other words, you can compute a value for the next y based on i+1
instead of i. But what good does this do if you cant compute i+1? The
trick is to compute it using the value of yi+1 generated by Equation [10.41];
that is, use the Adams predictor formula to predict a trial value for yi+1.
Using this trial value, you then compute i+1 and advance the differences
one step. Now you can compute a refined value for yi+1. This approach is
called the AdamsMoulton predictor-corrector method. You get the corrector formula in exactly the same way as the predictor. The corrector equivalent to Equation [10.41] is
2
[10.43]
y i + 1 = y i + 1 ---- ------ ------ h f i + 1 .
2 12 24
318
Second order
1
1
= --- ( 3 z )hD
2
1
1
= --- ( 1 + z )hD
2
Third order
1
1
2
= ------ ( 23 16z + 5z )hD
12
1
1
2
= ------ ( 5 + 8z z )hD
12
Fourth order
1
1
2
3
= ------ ( 55 59z + 37z 9z ) hD
24
1
1
2
3
= ------ ( 9 + 19z 5z + z ) hD
24
Fifth order
1
1
2
3
4
= --------- ( 1901 2774z + 2616z 1274z + 251z ) hD
720
1
1
2
3
4
= --------- ( 251 + 646z 264z + 106z 19z )hD
720
Error Control
319
Sixth order
1
1
2
3
= ------------ ( 4277 7923z + 9982z 7298z
1440
+ 2877z
475z )hD
1
1
2
3
= ------------ ( 475 + 1427z 798z + 482z
1440
173z
+ 27z )hD
Error Control
With numerical methods, control of errors is always an issue. In everything
Ive done here, Ive assumed that data was available in tabular form with a
uniform step size, h. But what is a reasonable value for h? And how do I
know that Im really computing meaningful results and not just random
numbers?
The options are not many and not very appealing. A common method is
to do everything at least twice: perform the computation with one step size,
then halve the step size and try it again. If the results are nearly equal, you
say, thats close enough. If they differ, you must halve again and continue
until things settle down.
Fortunately, in addition to its other virtues, the AdamsMoulton method
gives a virtually free estimate of the truncation error. Both the predictor and
corrector formulas would be exact and would give the same result if you
took enough terms in each series. Because you truncate the series to a finite
number of terms, both formulas contain some error. For the predictor, write
yi = (y)P + eP ,
320
so the two left-hand sides must be identical. Equating the right-hand sides
gives
(y)P + eP = (y)C + eC
or
[10.44]
The left-hand side of this equation is measurable its simply the difference between the integrals produced by the predictor and the corrector. You
have no idea what the errors are on the right-hand side, but its reasonable
to suppose that the dominant part of both errors is the first neglected term
in the series. In other words,
[10.45]
eP = Pnnhi
and
[10.46]
eC = Cnnhi+1,
where Pn and Cn are the coefficients of the first term neglected in each case.
Now, the error youre really interested in is eC because thats the error of
the last value computed and the one that youll end up using. If you make
the (reasonable) assumption that (x, y) doesnt change appreciably from
step i to step i + 1, you can divide the two equations to get
e
P
----P- = -----neC
Cn
or
[10.47]
Pn
e P = ------ e C .
Cn
Problems in Paradise
321
Cn
e C = ------------------ [ ( y ) P ( y ) C ] .
C n P n
This formula states that by subtracting the two estimates of y, you can
compute an estimate of the truncation error left by the corrector. Its worth
noting that, having computed this estimate, you can also correct the corrector by adding in this estimate. Thus, you not only gain a good estimate
of the error, but you can actually improve the accuracy as well.
Recall that the coefficients Pn and Cn are the coefficients of the powers of
the first neglected terms. In Table 10.8, these are tabulated for various
orders, and the ratio
Cn
----------------C n Pn
is given. To get the estimated error, simply take the difference of the two
ys and multiply them by the factor corresponding to the first ignored
order.
Table 10.8
Error coefficients.
Order
1
2
3
4
5
6
7
8
Pn
1/2
5/12
3/8
251/720
95/288
19087/60480
5257/17280
1070017/3628800
Cn
1/2
1/12
1/24
19/720
3/160
863/60480
275/24192
33953/3628800
Cn/(Cn Pn)
1/2
1/6
1/10
19/270
27/502
863/19950
1375/38174
33953/1103970
Problems in Paradise
So far, the AdamsMoulton predictorcorrector method seems ideal. Its
extensible to any order, gives fast and accurate results, and gives an almost
free estimate of the error incurred at each step. Who could ask for more?
322
Interpolation
323
Are you beginning to get the idea that theres more to simulation than
merely having a good integration formula? Youre absolutely right, which is
why some people have devoted whole lifetimes to developing good integration routines. Clearly, I cant go into enough detail here to solve all the problems, but at least I can identify them for you.
Some years ago I tried to invent a self-starting AdamsMoulton integrator, and the results were very encouraging. The general idea is to try to build
up the set of differences that I would have gotten had I passed through the
initial point from a previous time, instead of merely starting there. The
method also permits step size control. I can give you the general idea here,
but first, take a look at the next major application of these methods:
Interpolation
Interpolation is the act of estimating the value of a function at some nonmesh point, given a table of values of that function. Youve probably done
this at one time or another in school, almost certainly using linear interpolation. In the more general case, youd like to have higher order methods that
allow you to interpolate to any degree of accuracy.
Of all the methods Ive looked at so far, this one is the trickiest, because
Im dealing with values of x that arent in my mesh; that is, they arent separated by the step size h. Nevertheless, youll find that my methods using
operator math will still serve.
For a start, go all the way back to Equation [9.57]. This time, instead of
letting x = xi + h, Ill introduce a partial step given by h, where I assume (but
dont require) that < 1. Going through the same process as before,
[10.49]
f (x i + h) = e
hD
f ( x i)
= z f (x).
f i = f (x i + h) f (x i)
f i = f (x i) f (x i h).
or in operator parlance,
[10.51]
= z 1.
324
so I can write
[10.52]
= (1 + ) 1.
( 1 + ) = 1 + + --------------------- + --------------------------------------- + ,
2!
3!
so I get
[10.53]
( 1) 2 ( 1)( 2) 3
= + --------------------- + ---------------------------------------
2!
3!
( 1)( 2)( 3) 4
+ --------------------------------------------------------- +
4!
If Equation [10.5] is the Rosetta Stone, this equation has to be a close second. Applying it to i gives the interpolated value at xi + h as desired. You
can get that interpolation to any desired degree of accuracy by choosing
where to truncate the series. But the formula also has more far-reaching
implications, as youll see in a minute.
Of course, as in the case of finding the derivative, you have a one-sided
solution, depending only on forward differences. I just as easily could have
expressed z in terms of backward differences to obtain
[10.54]
( + 1) 2 ( + 1)( + 2) 3
= + ---------------------- + ----------------------------------------
2!
3!
( + 1) ( + 2 ) ( + 3 ) 4
+ --------------------------------------------------------- +
4!
Paradise Regained
325
= ( 1 z )
Order 2
1
2
= --- [ ( + 3 ) 2 ( + 2 )z + ( + 1 )z ]
2
Order 3
2
1
2
3
= ----- [ 2 ( + 3 ) 3 ( + 3 ) ( + 2 )z + 3 ( + 3 ) ( + 1 )z ( + 2 ) ( + 1 )z ]
3!
Order 4
1
= ----- [ ( + 5 ) ( 4 + ( + 3 ) ( + 2 ) ) 4 ( + 4 ) ( + 3 ) ( + 2 )z +
4!
6 ( + 4 ) ( + 3 ) ( + 1 )z
4 ( + 4 ) ( + 2 ) ( + 1 )z
+ ( + 3 ) ( + 2 ) ( + 1 )z ]
Paradise Regained
I said earlier that there were cures to the problems associated with the
AdamsMoulton method, these problems being the need for a starting
mechanism and the difficulty of changing step size. Both problems can be
fixed using a method hinted at by Equation [10.54]. If you use this equation
to operate on i as usual, you use it as an interpolation formula. But look at
the equation again. Standing alone, it expresses the difference for a new step
size, h, in terms of backward differences for the old step size. A similar formula holds for new backward differences.
[10.55]
( 1) 2 ( 1) ( 2 ) 3
= -------------------- + --------------------------------------
2!
3!
( 1) ( 2 ) ( 3 ) 4
--------------------------------------------------------
+
4!
326
Paradise Regained
327
their actual values perhaps wildly off and further iterations will be
necessary to trim them up.
The other alternative is to begin with a first-order formula for the first
step, a second-order for the second step, and so on. Indeed, many people
start their production multistep methods in this manner, although for reasons Ive already given, its a terrible idea. However, in this case I expect to
be iterating on the difference table, so its OK to use approximations the
first time through.
In a related question, maybe it would be better to use a single-step
method for the entire N steps (where N is the order of the integration) for
one cycle of integrate/reverse. Although this wouldnt give me a good
enough table in a single cycle to start the integration in earnest, it may be
the most stable way to get a first guess of the starting table. On the next pass
I could use a second-order formula, the next pass a third-order, and so on,
until the entire table is built.
From the tests Ive run so far, it appears that altering the order as I start
up is a better approach than starting with the full order. It makes early
determination of the higher differences a little more stable; therefore, it converges faster. On the other hand, it also complicates the algorithm considerably.
The final decision I should make is, how do I know when to stop iterating? This is one question I think I already know the answer to: its safe to
start integrating in earnest when the estimated error is within bounds. But
that answer leads to yet another question: what if the estimated error is
never within bounds? In other words, what if I start with too large a step
size in the first place? A key duty of any numerical integration algorithm is
to make sure the initial step size is small enough to get good accuracy on
that first step. But if Im busy iterating on a starting table and using the
error as a measure of when to start moving away from the origin, I cant
also use it to decide the step size.
To summarize, the idea of a self-starting multistep method is still a work
in progress. Most available multistep algorithms are strictly fixed-step-size
algorithms and are not self-starting. They depend on some external helper,
usually based on a single-step (also known as a RungeKutta) method to get
going and to restart after a step size change. From the work Ive done so far,
Im convinced that a self-starting, step-size changing, multistep algorithm is
eminently feasible. However, Im still tinkering with the details of the algorithm and the most stable way to deal with the initial building of the difference table (or, equivalently, the negative time back values, in a z-based
implementation). The final algorithm is not yet ready for prime time.
328
A Parting Gift
As I close this odyssey through the topic of numerical methods, Id like to
leave you with a very simple integration algorithm thats served me well in
real-time simulations for more than 20 years.
Normally, in real-time systems youre limited to using only the predictor
equations, because data comes in as a stream of numbers and you have no
way of knowing what the future values will be. Recall that corrector algorithms depend on being able to estimate the value of the derivative function
at time t + h. However, in a real-time system that derivative function almost
surely depends on measurements that havent been made yet. Hence, corrector formulas dont usually help much in real-time systems.
Fortunately, theres an important exception to this rule, which depends
on the fact that most dynamic systems have equations of motion of second
order.
2
[10.56]
dx
dx
-------2 = f (t, x, -----)
dt
dt
You can reduce this to two equivalent first-order equations by introducing a new variable v.
dx
----- = v
dt
[10.57]
dv
----- = f (t, x, v)
dt
In a real-time system, you cant evaluate (t, x, v) at the new point ti+1
until you have vi+1 and xi+1. This means that you can only use the predictor
A Parting Gift
329
formula on the integral of dv/dt, to give vi+1. On the other hand, once
youve integrated the first equation using a predictor, you have the new
value vi+1, so you can use the corrector formula for integrating x. The results
for a second-order integral are
[10.58]
h
v i + 1 = v i + --- ( 3 f i f i 1 )
2
and
[10.59]
h
x i + 1 = x i + ---(v i + 1 + v i) .
2
I first derived these equations around 1970 while working for a NASA
contractor, and therein lies a tale. At the time, I was asked to modify one of
NASAs real-time simulations. Looking inside, I saw that they were using the
form of Equation [10.59] for both position and velocity. This is just plain
wrong; because they didnt yet have a value for i+1, they had no business
using the formula for a corrector. Stated another way, they were generating
data that was one step off of where it should have been.
The funny thing about errors like this is that they are easy to miss,
because to all intents and purposes, the program seems to be working. If
you make h small enough, even an incorrect formula is going to work just
fine. The only effective way to really test such things is to plot the error versus the step size and examine its slope on a loglog scale, as I did for the
errors in Figure 9.3. The programmer for NASA hadnt done that.
This aspect that things can appear to be right even when they arent
led to my next difficulty: convincing the programmer that hed goofed. After
all, not only did the simulation appear to be working correctly, NASA had
been using it for years. He didnt want to tell his bosses that the data it had
generated was suspect.
Add to that a common programmers failing, a reluctance to accept criticism from someone else, and you have a conflict. This one got pretty heated.
I was trying to show the programmer the math in Equations [10.58] and
[10.59], but he wasnt as interested in seeing a math derivation as he was in
seeing me disappear. In fact, he suggested rather convincingly that I might
get a bloody nose if I didnt remove it from his business. In the end, I had to
go over his head to coerce him into making the change. The change worked,
and worked well, but I felt Id made an enemy for life.
The funny part is, 10 years later I met the same programmer again. As he
rushed over to me, I wasnt quite sure whether I should greet him warmly or
run like blazes. Fortunately, all seemed forgiven, because he greeted me
330
References
331
References
Crenshaw, Jack. 1993. More Calculus by the Numbers, Embedded Systems Programming, 6(2): 4260.
Pierce, B.O. 1929. A Short Table of Integrals (third edition). Ginn and
Company.
332
11
Chapter 11
dy
----- = f (x) .
dx
Because this function does not depend on y, you can plot a graph of (x), and
integration finds the area under the curve. More appropriate to numerical
calculus, you can generate a table of values of the function and find the integral by operating on those tabular values. For this purpose, Simpsons Rule is
more than adequate for most applications.
Think of Equation [11.1] as a special case of the more general and more
challenging problem in which the function depends on y as well as x.
[11.2]
dy
----- = f (x, y)
dx
333
334
dx
------ = f (x, t) ,
dt
x(0) = x0.
Multistep Methods
In Chapter 10, I gave one method for solving equations of this type: the
AdamsMoulton (A-M) predictorcorrector method. This method is based
on building a table of differences of previous and current values of f(x, t)
and summing them to get a predicted value of x(t), thereby inching forward
in time using small steps.
Recall in Chapter 10 that I derived the A-M formulas using an elegant
derivation based on z-transforms. The power of z-transforms is in allowing
you to derive equations that are easily extensible to any desired order, which
often equates to any desired degree of accuracy.
The A-M method is called a multistep method because it depends on the
results of previous steps to keep the difference table going. This gives the AM method both its greatest strength and its greatest weakness. By retaining
data from past steps in the difference tables, you need only one new evalua-
Single-Step Methods
335
tion of f to take the next step forward two evaluations if you apply the
corrector half of the formula. This is important because, for most realworld problems, Equation [11.3] can be very complex, so the time required
to do the differences and sums is trivial compared to that required to evaluate f. The fewer evaluations per step, the better.
The weakness lies in the table of differences. Because at t = 0 you dont
have any past values of f, you also have no table. Thus you are in the embarrassing position of having an elegant and powerful method that you cant
use because you cant take the first step. Over the years, various methods
have been devised to help kick-start multistep methods, but they all require
extra code and lots of care to make sure the accuracy of the whole computation isnt ruined by the first few steps. In Chapter 10, I outlined a scheme for
making the A-M method self-starting, and other analysts have devised similar schemes. Even so, getting such a method going is a bother and a potential source of devastating errors.
Also, because the difference table indeed, the whole idea of z-transforms on which the method is based depends on the assumption that all
integration steps are the same size, its practically impossible to change the
step size during a run. To do so is tantamount to starting all over again with
the new step. Because real-world problems arent always cooperative, the
error from step to step is often different for different values of time. So you
have a serious dilemma: because you cant easily change step size, you typically must run at the step size needed for the worst case part of the run. In
practice, the theoretical speed advantage of multistep methods is wasted
because changing the step size is so difficult. Again, in Chapter 10 I gave a
technique for changing the step size in A-M methods, but to use it requires
yet another layer of transformation algorithms.
Single-Step Methods
I wouldnt be telling you all this if I didnt have a solution: a whole separate
class of methods that fall under the general umbrella of single-step methods,
often described as RungeKutta (R-K) methods. The classical way of kickstarting an A-M algorithm is to use an R-K starter. I first became involved
with R-K methods almost 30 years ago, for just that reason: I intended to
build a routine as a starter for a planned A-M program. As it turned out, I
found an R-K method that was so stable, accurate, and easy to use that it
completely met all my needs, and Ive never found a compelling reason to go
farther with that A-M program.
336
337
x = f (x, t).
Because the function has two dimensions, I can plot (x, t) on two axes,
with the value of as a third variable. But remember that is the same as x ,
which is the slope of the curve of the trajectory passing through any point.
Thus, I can depict the function as a two-dimensional field with little lines at
each point, as in Figure 11.1, to denote the slope. It looks like a fluid flow
field. As you might guess, the desired solution for a given initial value x0 is
the streamline that is everywhere parallel to those little lines. The Runge
Kutta method operates by probing the flow field at carefully selected
points and using the information gained to come up with the best guess for
the next point along the trajectory. Because depends on both x and t, I
have to deal with partial derivatives instead of total derivatives, as for previ-
338
and
f
------ = f t .
t
Figure 11.1
Flow field.
Now I ask myself how to compute the derivative of , which Ill need for
the higher order formulas. The chain rule from calculus gives the answer.
df
f f dx
------ = ----- + ------ ----dt
t x dt
First Order
339
Note that the last derivative is the total derivative x , which Equation [11.5]
tells me is the same as . In a more economical notation, I can write
[11.6]
f = f t + f f x .
This neat little formula is the closest Ill find to the Rosetta Stone z-transform that I used for the A-M derivation.
First Order
As in all other integration formulas, the first-order R-K is trivial. In fact, for
first order, all formulas are identical. Ill still go through the process of
deriving it, because doing so will give me a chance to introduce the notations and concepts that characterize all higher orders.
The Taylor series, on which all integration formulas are based, provides
all the guidance needed to compute the first-order solution. That series can
be written
2
[11.7]
h x h
x
x(t + h) = x + hx + -------- + ---------- + ,
2
3!
where h is the integration step size, and x and all its derivatives are evaluated at the current value of t. Because x = f (x, t), you can write
[11.8]
2
3
h f h f
f (t + h) = x + hf + --------- + --------- + .
2
3!
For the first-order solution, you need only the first two terms. In the peculiar notation common to R-K methods, write
[11.9]
k = h(x, t),
so
[11.10]
x(t + h) = x + k.
this formula is the same as all other first-order formulas. At higher orders,
however, youll find that the R-K method begins to show its true colors.
340
Second Order
The Taylor series for second order is
2
[11.12]
h
x(t + h) = x + hf + ----- f .
2
[11.13]
h
x(t + h) = x + hf + ----- ( f t + f f x ) .
2
This equation gives a formula for the second order, all right, but it leaves the
problem of how to find the partial derivatives. That is where the probing
comes in and where the nature of the R-K approach finally becomes visible.
For an order higher than Order 1, you can be sure that youll have to
evaluate at more than one point in the xt space. But what should the second point be? Because the slope of the trajectory at the starting point x is
know after the first evaluation of , it makes sense to use that slope for the
next probe. Indeed, its difficult to think of any other way of finding it. But
(and heres the important feature of the R-K method), you dont necessarily
have to extend the slope to the time point t + h any time will do. Ill
extend the slope as defined by to a new time t + h, which gives the following two steps:
[11.14]
k 1 = hf (x, t)
k 2 = hf (x + k 1, t + h).
Note that, whatever scale factor you use for t, you must also use for k1.
Its pointless to try different scale factors for the two, because you want to
progress along the slope line. Finally, assume (although its not guaranteed)
that the true value of x at t + h is given by a linear combination of the two
ks.
[11.15]
x(t + h) = x + a 1 k 1 + a 2 k 2
Now the problem is reduced to that of finding three coefficients, a1, a2, and
, and thats where all the rest of the work in deriving R-K equations comes.
The next key step is to recognize that k2 can be expanded into its own
Taylor series this time based on partial instead of total derivatives. You
may not have seen the Taylor expansion of a function of two variables
before, but it looks very much like the series in one variable, except with
Second Order
341
[11.16]
1 2
2
f (x + a, t + b) = f + a f x + b f t + ----- ( a f xx + 2ab f xt + b f tt )
2!
1 3
2
2
3
+ ----- ( a f xxx + 3a b f xxt + 3ab f xtt + b f ttt ) +
3!
Now you can begin to see why the derivation is so complex. Only keep the
first three terms to first order, which gives
[11.17]
k2 = h[ f + (k1 f x + h f t )] .
x(t + h) = x + a 1 hf + a 2 h [ f + ( k 1 f x + h f t ) ]
= x + ( a 1 + a 2 )hf + a 2 h ( f f x + f t ).
At this point, two separate equations have been derived for x(t + h):
Equation [11.13] and [11.18]. If the conjecture that the form of Equation
[11.15] is to hold, the two equations must agree. Setting them equal, and
equating coefficients of like terms, gives two conditions which the unknown
constants must satisfy:
a1 + a2 = 1
[11.19]
1
a 2 = --- .
2
342
Table 11.1
Second-order formulas.
=1
EulerCauchy
k 1 = hf (x, t)
k 2 = hf (x + k 1, t + h)
1
x(t + h) = x + --- ( k 1 + k 2 )
2
1
2
= ---
Midpoint
k 1 = hf (x, t)
1
1
k 2 = hf x + --- k 1, t + --- h
2
2
x(t + h) = x + k 2
A Graphical Interpretation
2
3
= ---
343
Huen
k 1 = hf (x, t)
2
2
k 2 = hf x + --- k 1, t + --- h
3
3
1
x(t + h) = x + --- ( k 1 + 3k 2 )
4
1
3
= ---
Crenshaw
k 1 = hf (x, t)
1
1
k 2 = hf x + --- k 1, t + --- h
3
3
1
x(t + h) = x + --- ( k 1 + 3k 2 )
2
As a matter of fact, this choice of coefficients is one of the more frustrating aspects of R-K methods: you have no proven way to show that any one
choice is better than another, so you always have this tantalizing feeling that
the truly best algorithm is yet to be found. For the second-order methods
Ive been discussing, it would be reasonably straightforward to try all possible values of , but as I go to higher and higher orders, the number of
degrees of freedom also increases dramatically. Instead of one free variable,
I might have five or 10. An exhaustive search for such cases is out of the
question.
A Graphical Interpretation
At this point, it will be helpful to try to visualize how the method works.
You can do that using a flow field graph like Figure 11.2. First, because
k1 is always given by the slope at the starting point, extrapolation always
begins along the tangent line for a distance h. In the case of the Euler
Cauchy algorithm, you go all the way to the next grid point t + h. Here k2 is
defined by the new slope. At this point, you can go back and erect a second
extrapolation using the slope implied by k2. Because the two ks are
weighted equally, the final estimate is given by a simple average of the two
slopes.
344
Figure 11.2
Euler-Cauchy method.
For the midpoint algorithm shown in Figure 11.3, the method is different. This time, extrapolate only to t + h/2, and compute k2 proportional to
the slope there. Then erect another slope line parallel to this slope and
extend it all the way to t + h. Note that in this formula, k1 doesnt appear in
the formula for x(t + h). You only use k1 (and the slope that generated it) to
find the midpoint, which in turn gives k2.
A Graphical Interpretation
Figure 11.3
345
Midpoint method.
You can draw similar graphs for the other cases that can help you see
whats going on, but in reality, once youve seen one such interpretation,
youve seen them all. The details of the graphical interpretation dont really
matter, because you only need to blindly implement the algorithm once the
coefficients are known. Still, this graphical interpretation does serve an
important function: It lets you see the probing process in action. Two
important facts emerge. First, note that, unlike the case with multistep
methods, you are probing at points that are not actually on the final trajectory. This gives you the idea that you are somehow learning more about the
nature of the flow field, which certainly ought to be a Good Thing. Similarly, except when = 1, you are also probing at nongrid spacings. Both of
these facts add to the stability of the method and explain why it outperforms A-M methods in that department. This stability issue may also
explain why off-grid values of , such as 1/3 or 2/3, seem to work a little better than others.
346
A Higher Power
From my analysis of the second-order algorithm, I think you can see how
the R-K method can be extended to higher orders. In the general case, compute the following.
k 1 = hf (x, t)
k 2 = hf (x + c 21 k 1, t + d 2 h)
[11.20]
k 3 = hf ( x + c 31 k 1 + c 32 k 2, t + d 3 h )
.
..
k n = hf ( x + c n1 k 1 + c n2 k 2 + + c n 1 k n 1, t + d 3 h )
x(t + h) = x + a 1 k 1 + a 2 k 2 + + a n k n
Note that each new ki is computed from already-existing values, and the
final solution is a linear combination of all the ks. Except for one small
detail, you could theoretically extend this concept to any desired order, just
as with the multistep methods. That small catch is that you have to come up
with formulas like Equation [11.19] to define the coefficients. To do that,
A Higher Power
347
[11.21]
1
a 2 c 21 + a 3 ( c 31 + c 32 ) = --2
1
2
a 2 c 21 + a 3 ( c 31 + c 32 ) = --3
1
a 3 c 31 c 32 = --6
Here are four equations in six unknowns, giving two degrees of freedom
instead of one. In practice, you almost never see examples of the third-order
R-K algorithm written down or used not because theres anything wrong
with using third order but because fourth order requires very little more
effort. For completeness, though, Ive included two possible algorithms for
third order in Table 11.2.
348
Table 11.2
Third-order formulas.
Algorithm 1
k 1 = hf (x, t)
1
1
k 2 = hf x + --- k 1, t + --- h
3
3
k 3 = hf ( x k 1 + 2k 2, t + h )
1
x(t + h) = x + --- ( 3k 2 + k 3 )
4
Algorithm 2
k 1 = hf (x, t)
1
1
k 2 = hf x + --- k 1, t + --- h
2
2
k 3 = hf ( x k 1 + 2k 2, t + h )
1
x(t + h) = x + --- ( k 1 + 4k 2 + k 3 )
6
Fourth Order
In the improbable case that you havent gotten the message yet, deriving the
equations for higher order R-K methods involves mathematical complexity
that is staggering, which explains why Fehlberg needed a computer to deal
with them. Fortunately, you dont have to derive the formulas, only use
them. If there ever was a case where you should take the mathematicians
word for it and just use the algorithms like black boxes, this is it. Im sure
you already grasp the general approach used in R-K methods, so without
further ado, Ill give you the two most oft-used fourth-order algorithms.
They are shown in Table 11.3. The Runge formula is almost always the one
used, because Runge chose the coefficients to make the formulas as simple
and fast to compute as possible. However, because of the 1/3 and 2/3 mesh
points used in the Kutta algorithm, I suspect it might be a bit more accurate
and stable. Of course, other choices of coefficients are possible; in fact, an
endless number of them. The Shanks formula is one sometimes quoted, but
its rarely used anymore because its only real virtue is that it requires the
storage of one less k vector. In the days when memory was tight, Shanks
method was very popular, but today it has little to recommend it.
Error Control
Table 11.3
349
Fourth-order formulas.
Runge
k 1 = hf (x, t)
1
1
k 2 = hf x + --- k 1, t + --- h
2
2
1
1
k 3 = hf x + --- k 2, t + --- h
2
2
k 4 = hf ( x + k 3, t + h )
1
x(t + h) = --- ( k 1 + 2k 2 + 2k 3 + k 4 )
6
Kutta
k 1 = hf (x, t)
1
1
k 2 = hf x + --- k 1, t + --- h
3
3
1
2
k 3 = hf x --- k 1 + k 2, t + --- h
3
3
k 4 = hf ( x + k 1 k 2 + k 3, t + h )
1
x(t + h) = --- ( k 1 + 3k 2 + 3k 3 + k 4 )
6
Error Control
Now I get to the big bugaboo of R-K methods: how to control truncation
errors. Recall that the A-M predictorcorrector method gave a virtually free
estimate of the error. Because the step size is so easy to change with R-K
methods, it would be delightful if you could get a similar kind of estimate
from them. Without an error estimate, your only option is to use a fixed
step size, which is simply not acceptable.
With a fixed step size, the only way to tell whether or not you have a
good solution would be to run it again with a different step size and compare the results. If theyre essentially equal, youre okay. One of the first
applications of the R-K method to provide step size control used a similar
approach. The idea is to take a single R-K step then repeat the integration
over the same interval, but using two steps with half the step size. Comparing the results tells whether the larger step is good enough. This method,
350
Comparing Orders
As an example, consider the first-order method,
[11.22]
k = hf (x, t)
x(t + h) = x + k,
k 2 = hf (x + k 1, t + h)
1
x(t + h) = x + --- ( k 1 + k 2 ).
2
For both methods, k1 is clearly the same (this will always be true). Subtracting the two expressions for x(t + h) gives a measure of the second-order
term.
e = x(t + h) second x(t + h) first
[11.24]
1
= x + --- ( k 1 + k 2 ) ( x + k 1 )
2
1
= ---(k 2 k 1)
2
Mersons Method
351
Mersons Method
Thirty years ago, when I was first browsing R-K methods for the A-M
starter, I came across an obscure paper in a British journal describing Mersons method. It looks like the fourth-order method from Table 11.4 and
requires the same number of function evaluations, but Merson claimed that
it gave a true estimate of the neglected fifth-order term. The algorithm is
given in Table 11.5, and the claim seems to be borne out by the appearance
of all the ks in the formulas for both x(t + h) and e.
The history of the method is interesting. Shortly after its publication, an
expert named Shampine published a rebuttal, claiming that Mersons
method doesnt really estimate all of the fifth-order term. He pointed out
that if the estimate were truly of the neglected fifth-order term, one could
add it into the computation for x and get a fifth-order R-K formula with
only five function evaluations a possibility that has been proven to be
352
Table 11.4
Third order
k 1 = hf (x, t)
1
1
k 2 = hf x + --- k 1, t + --- h
3
3
k 3 = hf ( x k 1 + 2k 2, t + h )
1
x(t + h) = x + --- ( 3k 2 + k 3 )
4
1
e = --- ( 2k 1 + 3k 2 k 3 )
4
Fourth order
k 1 = hf (x, t)
1
1
k 2 = hf x + --- k 1, t + --- h
2
2
1
1
k 3 = hf x + --- k 2, t + --- h
2
2
k 4 = hf ( x k 1 + 2k 2, t + h )
k 5 = hf ( x + k 3, t + h )
1
x(t + h) = --- ( k 1 + 2k 2 + 2k 3 + k 5 )
6
1
e = --- ( 2k 2 + 2k 3 k 4 + k 5 )
6
Mersons Method
353
What is the truth? I suspect that Shampine is right. Similar to my algorithm for second order, Mersons method only gives a true estimate of the
fifth-order term when the equations dont have certain cross-product partial
derivatives. But when I implemented both methods and tested them on realworld problems, I found that Mersons method was about 10 times more
accurate than the much more complex method of Shampine. Not only that,
it was also about 10 times as accurate as the ordinary RungeKutta method!
Whats more, the error estimate seemed to be reliable. I was so impressed
that Ive been using it ever since, Shampines warning notwithstanding. As a
matter of fact, I accepted Shampines challenge and added the error estimate
to the computation for x. This should have made Mersons method behave
with fifth-order characteristics, and indeed it did, for the test cases I used.
My guess is that the estimate takes care of only part of the fifth-order term,
but for most practical problems, its the dominant part. Even though its not
truly fifth order, call it a very good fourth-order method or perhaps a fourand-a-half order method; whatever it is, it works just fine, and its the
method I recommend to you.
Table 11.5
Mersons method.
k 1 = hf (x, t)
1
1
k 2 = hf x + --- k 1, t + --- h
3
3
1
1
1
k 3 = hf x + --- k 1 + --- k 2, t + --- h
6
6
3
1
3
1
k 4 = hf x + --- k 1 + --- k 3, t + --- h
8
8
2
1
3
k 5 = hf x + --- k 1 --- k 3 + 2k 4, t + h
2
2
1
x(t + h) = --- ( k 1 + 4k 4 + k 5 )
6
1
e = ------ ( 2k 1 9k 3 + 8k 4 k 5 )
30
354
Table 11.6
Fehlbergs method.
k 1 = hf (x, t)
1
1
k 2 = hf x + --- k 1, t + --- h
4
4
3
9
3
k 3 = hf x + ------ k 1 + ------ k 2, t + --- h
32
32
8
1932
7200
7296
12
k 4 = hf (x + ------------ k 1 ------------ k 2 + ------------ k 3, t + ------ h)
2197
2197
2197
13
439
3680
845
k 5 = hf (x + --------- k 1 8k 2 + ------------ k 3 ------------ k 4, t + h)
216
513
4104
8
3544
1859
11
k 6 = hf x ------ k 1 + 2k 2 ------------ k 3 + ------------ k 4 ------ k 5, t + h
27
2565
4104
40
25
1408
2197
1
x 4 = --------- k 1 + ------------ k 3 + ------------ k 4 --- k 5
216
2565
4104
5
16
6656
28561
9
2
x 5 = --------- k 1 + --------------- k 3 + --------------- k 4 ------ k 5 + ------ k 6
135
12825
56430
50
55
e = x 5 x 4
x ( t + h ) = x + x 4
12
Chapter 12
Dynamic Simulation
The Concept
In the last couple of chapters, Ive discussed a number of methods for
numerical integration. Ive given you, in considerable detail, formulas for
both multistep (AdamsMoulton) and single-step (RungeKutta) methods.
What I havent done is fully explain why these formulas are so important
and why Im so interested in them. You also havent seen many practical
applications of them things you could apply to your own problems. In
this final chapter of the book, I hope to make these points clear and to provide you with practical, general-purpose software for use in solving problems in dynamic simulation.
Youve heard me mention this problem in passing in previous chapters,
but until now, I havent formalized the concept. In dynamic simulation, you
are given a set of equations of motion, which can always be cast into the
form
[12.1]
dx
----- = f (x, t) ,
dt
355
356
x = f (x, t) ,
where x is some scalar or vector (array) variable, often called the state vector, and t represents time. Because the derivative of x depends on x as well
as t, you cannot solve this kind of problem using quadrature; you can only
begin at the initial state,
[12.3]
x(0) = x 0 ,
and propagate the solution forward in time. In Chapter 11, you saw that
this solution is equivalent to solving for one specific streamline in a (possibly
many dimensional) fluid flow (see Figure 11.1).
Why is this class of problems so important? Because this is the way the
universe operates. Objects in the real world obey the laws of physics, and
those laws almost always can be cast into the form of differential equations
like Equation [12.2].
I should mention here that most problems in physics are given as secondorder, rather than first-order, differential equations. For example, Newtons
second law of motion can be written,
[12.4]
F
x = ---- ,
m
v = x .
F
v = ---m
x = v.
Given a set of equations of motion for a dynamic system and a set of initial conditions, solving them predicts the future state of the system for all
future times. In short, you can simulate the system. A dynamic system
The Concept
357
changes in a predictable way with time. It could be anything from a golf ball
to a Mars-bound rocket; from a racing car to a decaying hunk of uranium.
If the system is something that flies, the equations define the path that
it takes through the air or space (the trajectory). Although you dont normally think of the path of a tank or the motion of a washing machine as its
trajectory, the principle is the same, in that everything about the systems
motion can be described by a time history of x; that is, x as a function of
time, or x(t).
Simulations are useful because they let you know what to expect of a system before you actually build and activate it. For example, it helps to know
that the Space Shuttle will fly before it is launched or that a pilot taking off
in a Boeing 767 for the first time will be able to get it down again.
The first person to compute a simulation was the first person who could:
the Kings own personal computer, Isaac Newton, who first applied the calculus he had invented to compute the motion of the planets (long before his
twenty-fifth birthday). Considering first the case of two-body motion
that is, the sun plus one planet Newton got a closed-form solution, which
proved that planets follow elliptical orbits and obey the empirical laws
known as Keplers laws.
When he attempted to apply the same technique to more complex systems (say, the Sun, Earth, and Moon, the three-body problem), Newton discovered what most people quickly discover, much to their dismay: the
problem cannot, in general, be solved in closed form. Only a very few cases
such as the two-body problem and the spinning top can be solved that way,
and even then, the solution is often extremely tricky. Although closed-form
solutions are delightful things to have (because you then have the equations
for all trajectories, not just one), they are generally out of reach. Therefore,
to predict the motion of even slightly more complex dynamic systems, you
have no choice but to do so via numerical integration. This is why the formulas given in Chapters 10 and 11 take on such great importance.
Newton went on to compute trajectories of cannonballs for the British
Army, thereby becoming one of historys first defense contractors (but by no
means the first Archimedes has that honor).
It is not difficult to see why the trajectories of cannonballs and cannon
shells are important to any society sufficiently advanced to own cannons.
Its a matter of life and death to be able to know where the cannon shell is
going to land. Its rather important, for example, that it lands on the
enemys troops and not your own. Artillery is targeted, at least partially,
with reference to firing tables, which give the elevation needed for targets at
358
359
The Basics
The fundamental differential equation one seeks to solve in numerical integration is shown in Equation [12.2]. The variable t is the independent variable because the equation doesnt describe the variations in t, but only in x.
For similar reasons, x is called the dependent variable because its value
depends directly on t and indirectly on itself. The dependent variable can
represent any quantity that youd like to measure. The independent variable
can also be any quantity, but it usually represents time, and thats what Ill
assume in the rest of this chapter for simplicity. In the special case where t
does represent time, I will use the shorthand, dot notation shown in
Equation [12.2].
The job of a numerical integration algorithm is to integrate equations in
the form of Equation [12.1]. Expect to see a solution in terms of a time history of x; that is,
[12.7]
x = g(t, x 0) .
360
dx
x
----- = ------ .
dt
t
dx
x ----- t = xt = hx ,
dt
where h replaces t, again to simplify the notation. But dont forget that you
.
have the value of x from Equation [12.1], so you end up with
[12.10]
x = hf (x, t) .
If you have some value of x, say xn, the next value will be
[12.11]
x n + 1 = x n + hf (x, t) .
To compute the time history of x, all you need is an initial value x0, a procedure to evaluate the derivative function (t, x), and a way to continually
add the small deltas that result. Ill build a program to do that for the special case where
[12.12]
f ( x, t ) = x .
(Why this function? First, because its simple. Second, because it has a
known analytical solution. When testing any analytical tool, it helps to test
it with a problem that you already know the answer to.)
In this case, the problem is so simple, at least on the surface, that you can
write the solution program in only a few lines of C++ code, as shown in
Listing 12.1.
As simple as this code fragment is, it contains all the features that numerical integration programs must have: a section to set the initial conditions, a
formula for computing the derivative, a loop to add successive values of x,
361
and some criterion to decide when its done. Because time is relative anyway,
the initial time is usually set to zero, as I do here.
Id like to point out two very important features of this program. First,
note that I have a statement to effect the printing of the output values.
After all, a program that computes the history of x isnt much good if it
never shows that history. Second, note that:
Time is always updated at the same point in the code that x is updated.
Never, but never, violate this rule. As obvious as the point may be, getting x
out of sync with t is the most common error in implementations of numerical integration algorithms, and I cant tell you how many times Ive seen this
mistake made. To avoid it, some go so far as to combine t with x (as a vec.
tor) and simply use the relation t = 1 to let the integrator update t. This
.
approach tends to hide the functional dependence of x on both x and t.
Still, the idea does have merit, in that it absolutely guarantees that x and t
can never get out of step.
362
Howd I Do?
If you compile and run the program of Listing 12.1, youll see it print two
columns of numbers: the first t and the second the corresponding value of x
at t. The last two values my version of Borland Turbo C++ printed out were
as follows.
t
1.999998
x
7.316017
x = x0 e .
363
has a definite place in the scheme of things, though soon I will show you a
much more accurate method that is not much harder to implement.
364
Home Improvements
Changing the halt criterion is the easiest problem to correct: Ill simply
replace the statement with the following.
while (t <= 2.0){
Unfortunately, now I have the opposite problem than before: The integrator will appear to go one step past the last time to t = 2.009999. This is a
problem common to all integration routines, and there is really no cure. Its
the price you must pay for using floating-point arithmetic, which is always
approximate. You would not see the problem if h were not commensurate
365
with the final time, but because people tend to want nice, even values, like
2.0 and 0.01, this problem will plague you forever. More about this later.
As I said earlier, I hope by showing you the kinds of subtle troubles you
can get into, you will come to appreciate that things must be done carefully.
Ive seen many implementations of integration methods in which the integration package takes only one integration step, and its left up to the user to
write the code for looping and testing for the end condition. I hope youll
agree by now that this logic is much too important and tricky to leave to the
user. It should be included in the integration package.
To remove the derivative function from the mainstream code, simply use
the idea of the function f(x, t) as its used mathematically. You can write:
double f(double t, double x){
return x;
}
The use of this function is also shown in Listing 12.2. In practice, its not
a good idea to have a single-character function name. Theres too much
chance of a name clash, so perhaps I should think of a better name. For historical reasons (my history, not yours), I always call my subroutine deriv().
Note carefully the order of the arguments in f() [or deriv()]: theyre in
the opposite order than in the equations. The reasons are more historical
than anything else, but in general, I like to write my software with the independent variable first. If you find this a source of confusion, feel free to
change the order.
A Step Function
Ill remove the integration algorithm from the mainstream code and write a
procedure that takes one step of the integration process.
void step(double & t, double & x, const double h){
x = x + h * deriv(t, x);
t = t + h;
}
366
Cleaning Up
The final step for this exercise is to get rid of the literal constants, or at least
encapsulate them. Because I have two kinds of variables, the state variables
such as t and x and the control variables such as h and the maximum time,
Ive chosen to initialize these in two separate procedures. I could easily read
in all four variables, but because t is almost always going to be initially zero,
Ive chosen to simply set it to zero rather than always having to input it. For
this example, Ive also chosen to set h as a constant, although this is probably a questionable practice in general.
The final form, at least for now, is shown in Listing 12.4. If you compare
Listings 12.1 and 12.4, I think youll agree that the latter is a much more
readable, if slower, implementation. Its still a long way from a general-purpose numerical integrator; in fact, its not a callable function, but a main
program, and the control logic is still embedded in that main program. Ill
367
deal with that problem in a moment. Nevertheless, the code of Listing 12.4
is clean, simple, and modular. Lest you think its still a trivial program,
please bear in mind that you can replace step() with a function that implements a single integration step to any desired order, without changing anything else. Likewise, you can implement any set of equations of motion, by
making x in main() an array instead of a scalar.
368
Generalizing
At this point, I have a stand-alone computer program which, though
extremely simple, has two things going for it:
It is correct; that is, it generates no erroneous results.
It is extensible to higher orders and different problems.
My next step will be to convert the computer program to a general-purpose, callable function that can be used as part of an analysts toolbox.
Before doing that, however, Id like to digress and reminisce for a moment.
This digression will help you to understand both the history of and the reason for such simulation tools.
I mentioned at the beginning of this chapter that the solution of equations of motion has a long history, going all the way back to Isaac Newton
and Charles Babbage. Much of the uses of computers during the early days
of Eniacs and Edsacs was devoted to the solution of such problems. In other
words, they were used for dynamic simulation.
In those early days, it never occurred to anyone to build a general-purpose integration routine. Each simulation was built to solve a specific problem, with the equations of motion hand coded into the program, usually in
assembly language. My own introduction to dynamic simulation came at
NASA, ca. 1959, using an IBM 702 to generate trajectories to the Moon. At
that time, I had never heard of FORTRAN; everyone used assembly language. Also in those days, computer time was very expensive, so the watchword was to save CPU time at all costs, even if it meant considerable extra
work for the programmer. To gain maximum speed, programmers tended to
eschew subroutines as much as possible, in favor of in-line code, hand-tailored to the current problem.
The time rapidly approached, however, when the time to produce a
working product took precedence over execution time as the factor of greatest importance. We soon discovered FORTRAN, which made life easy
enough to allow us the luxury of writing more elegant and reusable software.
Generalizing
369
During that period, someone a lot smarter than I took another look at
Listing 12.2 and noticed that we were always solving the same kinds of differential equations every time. Why, he asked, didnt we just write one simulation, once and for all, and just change the equations to fit the problem?
The idea seems obvious now, but at the time it was about as unlikely as
developing draggable icons under CP/M. The technology just wasnt there.
Thanks to the development of high-order languages like FORTRAN, and
the development of larger and faster mainframes, the idea soon became
practical. My first exposure to a general-purpose integrator was in 1961,
when one of the developers of the Naval Ordnance Lab routine, NOL3,
introduced it to my company. NOL3 was a FORTRAN subroutine that did
all the hard work of simulation. It was the first of what you might call an
executive numerical integrator; Once called, it took over the computer and
remained in charge until the problem was solved. You wrote subroutines to
evaluate the equations of motion [f(t, x) in Listing 12.2], to write output to
a printer, and so on, then you wrote a main program that simply set up the
initial conditions, performed a little housekeeping, and called NOL3. The
integrator did the rest. By changing the code for initialization, output, and
evaluating f(t, x), you could solve any problem in dynamic simulation.
Thanks to the ability to call subroutines (or, more correctly, pass parameters) in FORTRAN, you really didnt give up much in the way of performance efficiencies. It was a great idea. I never used NOL3 much, but I was
smitten by its concept, and later I shamelessly plagiarized it.
370
Real-Time Sims
371
like my QUAD series are anachronisms. Still, like many anachronisms, they
serve a useful purpose. If you need a quick solution to a dynamic problem
and you dont have the cash or the computing power to support a commercial simulation language, its awfully nice to have a tool like QUAD3 in your
pocket. Thats what the current focus of this chapter is all about.
Real-Time Sims
This reminiscence wouldnt be complete without the mention of real-time
simulations. When youre computing the orbits of comets, or even moon
rockets, the last thing in the world you want is a real-time simulation,
because you dont normally have eighty years to wait for the answer. But for
airplanes, spacecraft, and the like, where a pilot is involved, only real-time
simulation will do. I probably dont need to explain to you the advantages
of real-time simulations. Training simulators are used today for everything
from airliners to supertankers to the Space Shuttle. Pilots could not be
trained to captain such sophisticated and complicated systems without
them. You dont send ships captains out to run a supertanker until theyve
demonstrated that they can do so without running into something. The
Maritime Commissions ship simulator at Kings Point, Long Island, has
been training future captains for decades. Today, Im told that even Formula
1 racing drivers hone their skills with super-accurate simulations of both the
racing car and the track it runs on.
Early simulations, like those of the Link Trainer, were implemented with
all-analog mechanisms because it was the only option. Even well into the
1970s, plenty of simulations were still being done in analog computing labs.
But everyone saw the advantages of computing digitally, and digital, realtime simulations began to be constructed almost the day after the hardware
was capable of computing in real time. I recall such a system being built by
NASA as early as 1960.
Not surprisingly, real-time simulations have different needs than off-line
computer simulations. First and foremost is that numerical algorithms, like
the RungeKutta method, developed for studying the motions of comets
simply dont work in real time. They involve predicting ahead to future
positions and values that dont yet exist; therefore, different algorithms
must be devised. Fortunately, as I mentioned earlier, the demands on such an
algorithm are not stringent because any human operator in the loop, or control system for that matter, will correct any errors introduced by a marginally accurate algorithm.
372
Control Systems
So far, Ive talked only about simulations, but I dont want to leave you with
the impression that these are the only applications for numerical integration
algorithms. The other important area is control systems. Although the most
modern techniques for control are based on digital (z-transform) techniques, a lot of engineers still specify control systems, filters, and so on using
Laplace transforms and transfer functions. In such functions, every term
involving the Laplace parameter s translates to an integral in the time
domain, so control of a complex system typically involves the real-time integration of a fairly large number of internal parameters. The most famous
integrating controller is the well-known PID controller for second-order systems, but more complex control algorithms are also common. Fortunately,
the techniques developed for simulation of real-time systems work just as
well when integrating the internal parameters of an s-domain controller, so
you dont have to invent new methods to handle control systems.
Back to Work
Now that youve had a glimpse into the background and history of numerical integration modules, I hope youll see how they apply to the real-world
problems of embedded systems and why Im dealing with them in this book.
Having been thus motivated, Ill return to the problem at hand and finish
developing the software. When last seen, the software was in the state given
in Listing 12.4.
The routines of this listing are certainly simple enough, and they get the
job done, but the architecture still leaves a lot to be desired. The main
objection is that, contrary to my desired goal, I dont really have a generalpurpose integrator here. All of the logic is still wrapped up in the main program, and you have already seen that the logic to decide when to stop can
be tricky mainly because of the round-off error associated with floating-
Back to Work
373
point arithmetic too tricky, in fact, to require the user to deal with it. So
you should encapsulate the logic into a separate function called by main().
void integrate(double & t, double & x, const double tmax,
const double h){
out (t, x);
while (t < tmax){
step(t, x, h);
out (t, x);
}
}
Now the main program only needs the initial state and control parameters like tmax and the step size h before it calls integrate().
void main(){
double t, x, h, tmax;
init_state(t, x);
get_params(h, tmax);
integrate(t, x, tmax, h);
}
As simple as this program is, its beginning to take form as the foundation for an integration mechanism that you can use for many applications. I
know it doesnt look like much yet, but trust me on this: Im closer than you
think.
Two things are worth noting. First, as integrate() is written, it appears
that I can always apply a higher-order formula merely by rewriting step().
As a matter of fact, Ill do that very soon. Second, note that, like its ancestor, NOL3, the integrator itself is an executive routine: Once given control,
it never relinquishes it until the solution is complete. The functions deriv()
and out() are called only by the integrator as it needs them and never by the
main program. This turns out to be a very important point. If there was ever
an argument for information hiding, this is it. Recall that single-step
(RungeKutta) methods operate by evaluating the derivative function at
times and with states that are not necessarily correct (see Figure 11.3). Likewise, even though multistep methods nominally perform only one function
evaluation per step, the combined AdamsMoulton, predictorcorrector
374
Back to Work
375
step() and out(). A modified version of the integrator takes care of the
problem:
void integrate(double & t, double & x, const double tmax,
const double h){
double v;
v = deriv(t, x);
out (t, x, v);
while (t < tmax){
step(t, x, v, h);
out (t, x, v);
}
}
Here, Ive added the local variable v to hold the derivative, and this variable is passed as a reference parameter to both step() and out(). Of course,
this means that these functions must be modified to deal with the extra variable.
void step(double & t, double & x, double & v,
const double h){
x = x + h * v;
t = t + h;
v = deriv(t, x);
}
void out(const double t, const double x, const double v){
cout << t << ' ' << x << ' ' << v << endl;
}
376
Higher Orders
It may seem to you that the structure Ive given is awfully busy. Including a
new variable for the derivative seems a lot of bother to go through just to be
able to print it, and it also seems that Im passing a lot of parameters
through calling lists.
Youd be right on all counts. The structure is rather cumbersome for the
simple case Ive been working with, but I think youll see the wisdom of the
approach as soon as I start getting serious and tackle nontrivial problems.
To see how nicely things work, Ill go to a fourth-order RungeKutta integration. The formula for this algorithm is, youll recall,
k 1 = hf (t, x)
1
1
k 2 = hf t + --- h, x + --- k 1
2
2
[12.14]
1
1
k 3 = hf t + --- h, x + --- k 2
2
2
k 4 = hf ( t + h, x + k 3 )
1
x(t + h) = --- ( k 1 + 2k 2 + 2k 3 + k 4 ).
6
Note that I write the arguments of the function () in the opposite order
than elsewhere in this chapter, so that it now matches the code. This order,
in fact, is conventional for RungeKutta integrators. Dont let the change
throw you.
From this RungeKutta formula, you can write the new version of
step() almost by inspection.
void step(double & t, double & x, double & v,
const double h){
double k1, k2, k3, k4;
k1 = h * v;
k2 = h * deriv(t + h/2.0, x + k1/2.0);
k3 = h * deriv(t + h/2.0, x + k2/2.0);
k4 = h * deriv(t + h, x + k3);
t = t + h;
x = x + (k1 + 2.0*(k2 + k3) + k4)/6.0;
v = deriv(t, x);
}
Back to Work
377
Again, notice the protocol required: Function step() expects to find the
correct value of the current derivative in the variable v, and it leaves it there
for the next cycle. So far, Ive made no attempt to optimize the code in
step() I can do a few things to reduce the number of computations, but
this version is clean, clear, and gets the job done. To see how well it works,
compile and run the program for x = 1, tmax = 10. Youll find the resulting
value to be 22,026.4657799, which is correct to nine digits. To see even
more accuracy, change the step size h to 0.001 and try it again. This time,
the result is 22,026.4657948049, which is exact to fourteen digits, about the
limit of the precision you can expect from double-precision arithmetic. The
integrator may seem simple and crude, but theres certainly nothing wrong
with its accuracy!
The complete listing for the latest version of the general-purpose integrator is shown in Listing 12.5. Between step() and integrate(), you have a
general-purpose numerical integrator in only 12 lines of executable code.
Not bad at all. Remember, all other functions are user-provided and are
really quite simple to implement, as this example shows.
378
void main(){
double t, x, h, tmax;
init_state(t, x);
get_params(h, tmax);
integrate(t, x, tmax, h);
}
I am by no means finished. So far, I can only integrate a single scalar variable. Ill take care of that in the next section by introducing the concept of
Affairs of State
379
state vectors. Ill also address a whole host of pesky little problems and
introduce new features that handle the kinds of things that distinguish a
good routine from a great one. Finally, Ill address the important issue of
step size control.
Affairs of State
For the next round of improvements to our integrator, I have four improvements in mind. Two of them are cosmetic, the other two profound.
The biggest weakness in the integrator as it stands now is that it can only
integrate a single state variable. In the real world, dynamic problems almost
always involve more state variables; even the simplest dynamic problem,
that of Newtons motion of a single point mass, requires two (see Equation
[12.6]). For such problems, the present integrator is worthless as it stands.
Fortunately, this difficulty is easily overcome. Youve heard me hint
before of the concept of a state vector. I havent discussed vector math yet,
and I will not do so until the next volume of this series. However, for the
purposes here, you are not required to know much about vector math or
vector analysis; the most complicated thing you need to do with vectors is to
add them or multiply them by a scalar.
This is one of the profound differences I had in mind. Although the kinds
of systems that obey a single, scalar equation of motion are rare, the kinds
of systems that obey it when the variable is a vector, x, are virtually limitless. As a matter of fact, the only systems that cant be described in the more
general vector form are those that require partial differential equations
rather than ordinary equations. And even these are solved, in practice, by
solving an equivalent set of ordinary equations that approximate the true
form.
An equation of motion for a single scalar variable x would be:
[12.15]
x = f (x, t) .
If the system had two such variables, the equations of motion would be
[12.16]
x1 = f (x 1, x 2, t)
x2 = f (x 1, x 2, t),
380
x3 = f 3 ( x 1, x 2, x 3, , x N , t )
.
.
.
xN = f N ( x 1, x 2, x 3, , x N , t ).
x =
x3 ,
.
.
.
xN
which is precisely the form used in Equation [12.2]. Therefore, one form is
merely a shorthand notation for the other, and no vector mathematics are
implied. For our purposes, a state vector is simply a good old-fashioned
FORTRANPascalC/C++ one-dimensional array.
The only difference is the way the indices are counted. Youll note that
the index in Equations [12.17] and [12.18] run from 1 to N, which is the
conventional numbering in mathematics. FORTRAN arrays follow the
same rules. As you know, however, the authors of Pascal, C, and C++,
knowing little about mathematics and caring less, chose to bless us with
indices that run from 0 to N 1. To mathematicians, physicists, and engineers, thats a frustration, but a minor one, and one we can live with.
Affairs of State
381
[12.19]
k 4 = hf ( t + h, x + k 3 )
1
x(t + h) = --- ( k 1 + 2k 2 + 2k 3 + k 4 )
6
Now you can see the extent of the vector math you must use. The last of
these equations shows it most graphically, where you must add the four vectors k1 k4. Even this addition is trivial: you merely add each pair of vectors, element by element.
From a programming perspective, you know that its almost as easy to
pass a vector (array) through a calling list as it is a scalar (some languages
wont allow array return types), so the code changes required to accommodate state vectors are minor. About the only real complication involves adding for loops to do the vector additions.
Some Examples
If a picture is worth a thousand words, an example has to be worth at least a
hundred. Youve already seen one example: the equation of motion of a single point mass, as given by Newtons second law, in one dimension. I showed
that the second-order differential equation, Equation [12.4], can be written
as two first-order equations, Equation [12.6]. At the time, I didnt make
much of the bold-faced symbols in these equations, used to indicate vector
parameters. Equation [12.6] can be thought of as two scalar equations, if the
382
x = r .
v
f (x, t) =
v
F .
---m
You can see in Equation [12.21] that the elements of the state vector may
be, in fact, vectors (or matrices, or quaternions, or tensors). This becomes
very important as I get into simulations of more complex systems, which
might include multiple components.
The next example will look familiar to control systems engineers.
[12.22]
x + ax + bx = F (x, x, t)
v = x
Affairs of State
383
to get
v + av + bx = F (x, v, t)
or
[12.24]
v = av bx + F (x, v, t) .
x = x
v
x =
v
av bx F (x, t)
= f (x, t) .
By now you should see that the definition of the elements of the state vector (and its dimension) and the function f(x, t) change from problem to
problem. The method of solving the equations numerically, however, does
not change. This is what makes a general-purpose numerical integrator so
universally useful.
As noted in Equation [12.20], each component of the state vector x is
a three-dimensional vector, so the state vector itself has six scalar components. Only three of these components are independent, however.
Remember that the velocity, v, was introduced to convert a second-order
differential equation to first order.
The number of independent components of the state vector is called the
degrees of freedom of the body. If the system is constrained (to move in a
plane, for example), the number of degrees of freedom, and therefore the
size of the state vector, will be correspondingly smaller.
Because all purely dynamic systems (i.e., systems governed by Newtons
laws of motion) are described by second-order differential equations, you
can expect the size of the state vector to be about twice as large as the
degrees of freedom. I say about here, because there are important cases,
especially in rotational dynamics, with representations that use even more
elements. More precisely, then, the size of the state vector will be at least as
large as twice the degrees of freedom.
In the analysis of a new dynamic system, a large part of the process is
determining the degrees of freedom and choosing a state vector that is the
best (usually meaning the most simple) representation of the motion. In
practice, there are two steps, each simple in concept.
384
Vector Integration
To make all of this work in software, you need to modify the algorithm to
deal with a state vector array rather than a single scalar variable. Fortunately,
thats an easy change.
Im going to take this transformation in two stages. First, Ill look at it in
the more traditional form that I (and many others before me, and after)
have used, in which the state vector is expressed as an ordinary array of scalars. This is the only approach available when using a language like FORTRAN, C, or non-Object Pascal, which probably includes most of you
reading this book. Later, Ill look at a more modern, object-oriented
approach, but because C++ or any other object-oriented language is rarely
available for real-time applications, its important to be able to get a good
implementation the old-fashioned way.
x = 2y
y = 2x
This case has a simple known solution. For the analytical solution, I will go
back the other way for a change, from first- to second-order equations. Differentiating the first equation again gives
2
x = 2y = 4 x .
Perhaps youll recognize this equation more easily in its normal form:
[12.28]
x + 4 x = 0 .
The Software
385
x0 = 1
y0 = 0 ,
x = cos 2t
y = sin 2t.
Thus, the motion describes a circle of radius one in the xy plane, with a
period of one time unit.
The Software
Now Ill transform the integrator so it can handle state vectors. Ill start
with a C-style typedef.
typedef double state_vector[N];
Using a typedef, of course, doesnt give the strong type checking that Id
like and expect from Pascal or C++, but remember, Im trying to do things
the old-fashioned way here, for the benefit of those who must. Because the
derivative function is no longer a scalar, I cant return it as the result of a
function call. Instead, I must redefine the derivative function to be void and
add the derivative vector as an argument:
void deriv(const double t, state_vector x,
state_vector x_dot){
const double two_pi =2.0*3.14159265358979323846;
x_dot[0] =
two_pi * x[1];
x_dot[1] = - two_pi * x[0];
}
You may wonder, as you read this code, why I chose to encode the value
two_pi in-line, instead of using the carefully developed constant.cpp file.
The answer is simply that I took the easy route. This problem is so small
that it easily fits in a single file. Doing it this way, I dont have to bother linking other object files. In a production system, Id delete the line defining
two_pi and link constant.cpp instead.
386
Although I mentioned this point earlier, its worth repeating here: the
derivative vector, here called x_dot, is encapsulated in the integration routine. Nobody has any business accessing it except as it is made available by
the integrator, to the derivative and output routines. This is a very important rule violated only by the young and the reckless.
The big changes are in step(), which must be modified to deal with vectors. This routine is shown in Listing 12.6.
The Software
387
Some points about this routine are worth noting. First, because you cant
perform arithmetic in the calling lists as you could with scalars, Ive had to
introduce a new temporary variable to hold the trial values of x. As in the
case of the derivatives, these values are strictly that: trial values. Remember
that the RungeKutta method sort of pokes around in the vicinity of the
current point, measuring the slopes in various directions, but these probings
have nothing whatever to do with the final value. They are used only by the
integrator as it implements the algorithm, and in general, they will not lie on
the final trajectory. Thats why I keep emphasizing that no one should ever
access these intermediate values. Its also why you must make sure that the
derivative vector is left in a clean state when you leave the subroutine;
hence, the last call to deriv(), which finally gives a value of the derivative
that corresponds to the updated state.
388
Does it work? You be the judge. Figure 12.1 shows the results for a crude
approximation with only four steps around the circle. As you can see, the
path doesnt follow the circle very well, displaying an obvious divergence
that would get worse and worse each cycle. Whats remarkable, though, is
that the curve looks anything at all like a circle, with so few steps. Thats the
power of a high-order integration algorithm. A lower order method would
have diverged much more dramatically.
With even as few as eight integration steps, I get results that, at least to
the eye, seem to stick to the circle quite well, as you can see from Figure
The Software
389
12.2. And the 64-step solution of Figure 12.3 is essentially perfect, diverging
by only about one part per million. For single-precision solutions, I generally use about 100 to 200 steps per cycle, which is more than adequate.
390
Whats Wrong?
Do you see anything with the software as it stands that isnt very nice? Aside
from the somewhat messy code that the for loops create, my approach
requires you to recompile the integrator for each new problem hardly the
kind of behavior youd expect from a general-purpose utility. The reason
has to do with those explicit for loops in function step that perform the
arithmetic for the algorithm. You could avoid the explicit loop ranges by
passing the dimension of the vectors as a parameter thats the way it was
done in the good ol FORTRAN days, but FORTRAN was particularly forgiving when it came to array dimensions. Basically, it just didnt care what
the dimensions were. Newer languages are more picky, and that causes
more problems. In this case, the use of declarations depending on the typedef is enough to make the software problem dependent.
Wouldnt it be nice if you could arrange a way to avoid recompilation?
As a matter of fact, you can. One obvious way is to simply pass a pointer to
the array (thats whats coming through the calling lists, anyway), and index
off of it. You can take care of local variables such as the ks in step() with
dynamic memory allocation. Ive done that in both my production Pascal
and C versions of this integrator. I wont go into all that here, though, for
one reason: you can do a lot better using the object classes of C++. Thats
what Ill be looking at soon. For now, Id rather spend the time on some
more utilitarian topics.
The Software
391
Crunching Bytes
Speaking of memory allocation: Im sure it hasnt escaped your notice that
Im using a fair amount of intermediate storage for the four ks, plus a temporary vector. In these days of multimegabyte memory, a few wasted bytes
arent going to matter because state vectors rarely get much larger than 20
or 30 elements, anyway. There was a time, though, when I would have
killed for an extra 100 bytes, and those memories still stick with me. I can
remember going over my old FORTRAN integrators with a fine-toothed
comb, doing my own graph coloring optimization algorithms by hand to
find a few spare bytes. Its probably frivolous to bother these days, but its
also rather easy to collapse the storage somewhat using unions to overlay
vectors that arent needed at the same time. A first glance at the Runge
Kutta algorithm of Equation [12.19] doesnt seem to leave much room for
compression. All four ks are needed to compute the updated position. But
look again. Except for this one computation, k1 is only used to compute k2,
k2 is only used to compute k3, and so on. You can remove the need to retain
these vectors by keeping a running sum for the new state update, as written
in pseudocode form below.
k1 = h * x_dot;
sum = k1;
temp = x + k1/2.0;
k2 = h * deriv(t + h/2.0, temp);
sum += 2 * k2;
temp = x + k2/2.0;
k3 = h * deriv(t + h/2.0, temp);
sum += 2 * k3;
temp = x + k3;
k4 = deriv(t + h, temp);
t = t + h;
x += (sum + k4)/6.0;
x_dot = deriv(t, x);
With a careful examination of this code, you can convince yourself that,
except for the running sum, all the intermediate vectors collapse into the
same storage location. That is, x_dot is used only to compute k1, k1 only to
compute temp, temp only to compute k2, and so on. By using unions, you
can use the same storage locations for all of them. This saves 5*N words of
392
A Few Frills
If I really want to call this a general-purpose integrator, I need more flexible
control over things like stop times and print intervals. The integrator might
be using step sizes like 0.00314159, but people like to see data printed at
nice, simple values like 0.5, 1.0, 1.5, and so on.
As it turns out, control over the time steps is one of the more bothersome
aspects of writing an integrator, and thats why so many people just do what
Ive done so far take the cowards way out and cut the integrator some
slack at edge conditions.
When you give the integrator a terminal time, tmax, its reasonable to
assume that it will actually stop at t = tmax. But if the integrator is always
taking a full, fixed step, as Ive allowed it to do so far, the best you can hope
for is that it will at least stop somewhere within a step size, h, past the
desired final time.
If you used a multistep method like AdamsMoulton, you would have to
learn to live with such behavior. Multistep methods depend on difference
tables, so the step size cant be altered easily. But RungeKutta is a singlestep method, which means that the step size for a given step can be anything
you like, within reason, of course. In this case, the stop any time after
tmax sort of behavior should be considered downright unfriendly, and it
deserves to be dealt with harshly. Personally, I find that the way the integrator stops is a good measure of its quality, and I dont have much patience
with those that dont know when to stop. Similar comments apply if you
specify even print intervals, as I will shortly.
Fixing the problem, however, is more difficult than you might think.
Remember, youre dealing with floating-point numbers here, so after many
steps, nice even values like 25 are more likely to be 24.99999. If you specify
a print at t = 25, and youre not careful, you can end up getting two prints,
one at 24.99999 and another at 25. Similar things can happen with respect
to the halt condition. In short, dealing with edge conditions is important
and separates the good integrators from mediocre ones.
The Software
393
The code in Listing 12.7 deals with the halt condition and is much more
civilized.
As you can see by the listing, Ive modified the loop to look ahead one
step to determine if the next step will be in the vicinity of t = tmax. If so, it
exits the loop and takes one more step with a step size that goes right to
tmax. For safetys sake, Ive also added a test at the beginning to kick you
out of the loop in the off chance that you started with t > tmax. This avoids
the possibility of trying to take some (potentially huge) negative time step.
The presence of eps deserves some explanation. Its there to take care of the
round-off problem. Without it, you would have to take a tiny final step to
get back to t = tmax.
Introducing the fudge factor eps allows you to take a step that is marginally larger than the specified step only 0.01 percent larger in this case.
This approach neatly avoids the double-print problem, as well as the tinystep problem, with a minimum of hassle. The whole loop is designed to
eliminate strange behavior at cusp conditions. Instead of testing the end
condition as the step is taken, the integrator looks ahead and calculates that
only one more step is needed. Then it exits the loop, takes that one and only
one step, and quits.
394
and
t = t + h;
you should arrive right at tmax. Unfortunately, expectations are not always
met when using floating-point arithmetic. To be on the safe side, and at the
risk of offending purists, I just go ahead and stuff the final value of t with
the value I know it should be anyway. For the record, the extra line made no
difference in the few tests I made using Borland C++ and a math coprocessor. Your mileage may vary.
I have one last comment with respect to Listing 12.7. At first glance, it
might put you off to see two calls to step() and three to out(), in what is
basically a single-loop program. You could reduce the number of calls by
using various artifices, such as a boolean loop complete flag often used in
Pascal programs. But after trying such approaches, I discarded them. All
other approaches saved duplicating the calls, but at the expense of extra
lines of code to test flags. In the long run, the most straightforward and
transparent solution, which faithfully represents what the code is actually
doing, won out.
As a matter of fact, these duplicated calls can be a blessing in disguise. In
the real world, you almost always want to do something special on the first
call to the output routine, such as print a set of column headers, for example, and often youd like to perform some terminal calculations as well. In
my old production FORTRAN routine, I did just that, calling out() with a
control integer to distinguish between first, last, and all other calls (including error conditions). The structure of Listing 12.7 makes such an approach
ridiculously simple.
Printing Prettier
If you can force the integrator to give a nice, round number the final time,
why not for the print intervals as well? So far, Ive simply called out() after
every step. For some problems, this could result in a lot of printout and
The Software
395
waste either trees or electrons, depending on your output device. Its far
better to give the user the option of controlling print interval and print frequency. For the print interval, you can use the same approach as for the terminal time. The subroutine shown below is virtually a copy of
integrate().
void print_step(double & t, state_vector x,
state_vector x_dot, double h, double hprint){
double eps = h * 1.0e-4;
double tprint = t + hprint;
while (t+h+eps < tprint)
step(t, x, x_dot, h);
h = tprint - t;
step(t, x, x_dot, h);
t = tprint;
}
This routine takes the place of step(), and calls that function as needed
to progress from the current time to a target time, tprint. This new routine
is called, instead of step(), by the integrator. Note that it does not effect
any output itself. Thats still taken care of by the integrator. In effect, the
integrator sees a (potentially) larger step size than that defined and prints
only after one of these larger steps has been taken.
Print Frequency
While Im dealing with print control, theres one last feature to add, which
requires some explanation. Later on, Ill be adding the capability for variable step size. This permits the integrator to choose its own step size, in
order to keep errors within bounds. For problems where the dynamics vary
strongly, depending on the current value of the state, its not at all uncommon to see the step size vary quite a bit during a run sometimes by several orders of magnitude. For such cases, a fixed print interval is not good; it
can cause you to miss all the interesting, rapid changes in state. On the other
hand, printing every time might use up a lot of paper or CRT display scroll
distance. The perfect solution is to give the user the option of printing every
n steps, where n is some number the user chooses. Fortunately, you can add
396
Here, Ive supplied print_step() with an extra argument: the print frequency. If the function takes that many integration steps to get to the next
print interval, it will execute its own call to out(). This approach has the
disadvantage of spreading the calls to out() over two function, whereas
before it was called only by integrate(). Still, this is the cleanest approach.
You can use the integrator with any combination of print frequency and
print interval. If you only want one of the options, simply make the other
value large. Unfortunately, this usage creates a new problem. If the print
interval is made larger than the value of tmax, you wont exit print_step()
until youve passed the terminal time. To avoid that undesirable result, you
must add a bit of insurance in the integrator to force a smaller hprint, if
necessary. The fix involves a one-line change.
hprint = min(hprint, tmax - t);
The Software
397
Summarizing
The complete listing of the integrator, the test case, and the ancillary routines developed thus far is shown in Listing 12.8. In addition to giving
excellent accuracy, it handles the terminal condition, as well as user-specified print intervals and print frequency in a civilized manner. You wont find
a much better fixed-step algorithm anywhere. To make it a truly production-quality routine, you only need fix the dependence on the type state_
vector and pass the length of the vector through the calling list.
398
The Software
399
400
A Matter of Dimensions
Before proceeding with the development of the integrator, I need to talk
about an important impediment to progress.
To an old FORTRAN programmer, one of the more frustrating, infuriating, and puzzling aspects of modern programming languages, and a scandal
of at least minor proportions, is their lack of support for arrays with varying dimensions, so-called conformant arrays. The lack is significantly more
puzzling because FORTRAN has had them from the get-go. This fact is one
of the more compelling arguments math analysts and engineers give for
hanging onto FORTRAN.
How could FORTRAN II support conformant arrays, but Pascal and
C++ cannot? Two of the reasons are features of these modern languages
(particularly Pascal), which do have much to recommend them, but can
sometimes get in the way of progress: strong variable typing and range
checking. Pascal compilers traditionally check to make sure that every variable is used in the manner that is appropriate for its type, as declared. They
also (some optionally) make sure that index values in arrays cannot go out
of bounds. Without some tricky coding, its well nigh impossible to defeat
these attempts to save us from ourselves.
FORTRAN has no such problems because at the time it was invented (a)
the compilers werent sophisticated enough to check such things and (b) the
prevailing attitude then, unlike now, was not to try to save a programmer
from himself. The compiler translated code from source to object form. If you
happened to write stupid source code, you got stupid object code. Although
the compiler made every attempt to generate tight code (the original FORTRAN compiler was among the most efficient ever written), optimization was
never thought to include turning bad source code into good object code.
A Matter of Dimensions
401
When you write library routines, you try to do them in such a way that
they can be used in any situation without change. Thus, vector operator
packages should work on vectors of any size and matrix multipliers or
inverters should work with matrices of arbitrary size (within reasonable
upper limits, of course). When FORTRAN became available, it didnt take
long to figure out how to trick the compiler into allowing such uses, even
though it wasnt explicitly written to support them. The trick had to do with
the fact that in FORTRAN, all variables in a subroutines calling list
were/are passed by reference, rather than by value. Modern languages do
exactly the opposite: by default, they pass by value, and C can do nothing
else.
If a variable is passed by reference, it can be changed, and that can be a
dangerous practice. It led to a very famous and insidious bug in FORTRAN
programs, in which a literal constant, such as 1, was passed to a subroutine,
which changed it. From then on, every use of a 1 in the program used the
new value instead, thereby leading to some very confusing results. Perhaps
thats the reason the designers of C chose to pass by value, although I suspect that the real reason was more mundane: Unlike the old IBM mainframes for which the FORTRAN compilers were developed, the DEC
computer, on which C was developed, had a stack.
In any case, passing by reference is just what is needed for vector and
matrix arithmetic, because no local storage is required for the arrays; the
storage is allocated by whatever program calls the subroutines, and the subroutine modifies only the referenced array.
When you program in FORTRAN, its syntax requires that you declare
that an array is an array by using a dimension statement.
DIMENSION ARRAY(20)
However, it didnt take long to discover that the compiler really didnt
care what dimensions an array had; it only needed to know that it was an
array. No range checking was done, either, so the compiler really didnt care
if array range bounds were exceeded or not. To a FORTRAN compiler, the
code
DIMENSION X(1)
X(300) = 4.5
402
There was only one problem. This trick only works for one-dimensional
arrays (vectors). It doesnt work for matrices, because the compiler has to
compute the equivalent index by calculating an offset from the beginning of
the array (vector or matrix. Theyre all just a set of numbers to the compiler). For an array dimensioned M by N, with indices I (row) and J (column), the offset is
[12.31]
index = I + (J 1)M.
index = j + in.
A Matter of Dimensions
403
FORTRAN programmers who used such tricks understood full well that
they were taking advantage of a loophole in the compiler by using a characteristic that was never formalized and couldnt be counted on to work in
future releases. Fortunately, the practice became so universal that any compiler that didnt work that way would not survive in the marketplace.
Because of its widespread use, the programming trick became the de facto
standard.
When FORTRAN IV came out, the standard became formalized by a
mechanism called variable dimensions. You no longer had to resort to
tricks: the compiler writers built Equation [12.31] into the language. FORTRAN IV allowed the compiler to use the dimension data passed to it; thus,
you could write the following, for example.
SUBROUTINE MMULT(A, B, C, L, M, N)
DIMENSION A(L,M), B(M, N), C(L,N)
SUM = A(I,K)*B(K,J)
This feature removed the need for programming tricks. Although the
code ran only marginally faster (because the compiler could optimize the
indexing better), it certainly was a lot easier to read.
Sad to say, none of these tricks work in either Pascal or C. I think my
greatest disappointment with Pascal was to learn that this magical, beautiful, and modern language couldnt handle the simplest matrix math unless
the sizes of the matrices were declared in advance. If anyone had told me in
1960, when I was using the matrix-as-vector trick in FORTRAN and implementing Equation [12.31], that Id still be doing it in 1999, Id have laughed
right in their face. But, for C and C++ programs today, thats exactly what
Im doing. C and C++ will not even allow me to treat a matrix as a vector.
The two declarations
double x[9];
and
double A[3][3];
each allocate exactly the same storage: nine double words in sequential
locations. However, if you pass either array through a functions calling list
404
But look again. In addition to x, the integrator and its called functions
are littered with local vectors of equal size: k1, k2, k3, k4, temp, and x_dot
(two copies). A production-level integrator should allow you to allocate
these vectors at startup. My production versions of the quadN series, as
written in C and Pascal, use dynamic allocation to do just that (they also
have the number of local vectors needed, optimized down to the absolute
minimum).
Fortunately, most practical problems have only one state vector, which
has the same size throughout the entire program, and require only one
instance of the integrator. For the purposes here, the trick of defining the
size of the vectors in the typedef statement is sufficient.
Incidentally, the genius who wrote that old NOL3 integrator, which I so
shamelessly plagiarized, had what I considered a slick way of dealing with
variable-length, local storage: he didnt require it. Instead, he passed the
integrator a matrix of scratch-pad storage, equal to x in its row dimension. I
took that same idea a step further and simply required the user to allocate x
to be a vector of dimension 6N, where N was the real size of the state
vector. The user was supposed to access only the first N elements of x;
405
indeed, the rest of the vector didnt even exist as far as other functions, such
as deriv(), and out(), were concerned.
I found that approach to be eminently workable, and it relieves the integrator of any need to allocate storage dynamically (something FORTRAN
would have been unhappy to do for me, anyhow). My only problem came
from nosy users, who were forever poking around in that scratch pad and
trying to use it for other things. Users can be a pain in the neck, sometimes.
While Im discussing state vectors, theres another source of frustration
that you should be aware of. You saw in Equation [12.20] that practical
state vectors often include components that are themselves vectors. Alternatively, you might choose to declare the elements of the state vector to be
individually named scalars, as in the following example taken from a six
degrees of freedom (6DOF) simulation).
[12.33]
x
y
z
vx
vy
vz
x = q1
q2
q3
q4
p
q
r
The problem is that to the integrator, the state vector is no different than
any other vector. But to the analyst who writes the equations of motion
implemented by deriv(), its a set of 13 scalars, each of which has a name.
The programmer of deriv() would like to refer to these variables by name,
instead of looking them up in a data dictionary in order to remember that
variable p is really x[10]. The end result is code inside deriv() that is very
difficult to read. You cant tell the variables without a scorecard.
I must tell you, I have never really found a good solution to this problem.
FORTRAN does not allow you to reference individual elements of a vector
by name, although it does have equivalence statements. However, they are
not allowed in the context of passed parameters.
406
407
can return the value of the derivatives from a deriv(), as I did in the earlier,
scalar version of the integrator. Whats more, by defining deriv() as a
friend function, you can have direct access to the members of the state vector.
What does this mean? It means, mercifully, that the problem of getting to
the elements of the state vector by name goes away for good; you can refer
to the parts of the state vector by name, even if those parts happen to be
arrays.
For the moment, Ill just assume that Ive defined such a class, with all
the parts that it needs to operate. The only part you need be concerned with
is the definition of its member data items.
class State_Vector{
double x,y;
public:
};
Because Ive given the elements of the state vector names, I can refer to
them by name in the software I must write, such as in deriv().
State_Vector deriv(const double t, const State_Vector &X){
const double two_pi =2.0*3.14159265358979323846;
State_Vector temp;
temp.x =
two_pi * X.y;
temp.y = - two_pi * X.x;
return temp;
}
You must admit, this is a lot neater than the traditional version, where x
is a simple array. Notice that the judicious use of const and &, as well as the
class definitions, provide security and efficiency. While I was developing this
code, the const keyword saved me more than once from the generation of
unnecessary temporaries. I got messages, warning me of the problem that I
wouldnt have seen otherwise.
408
Most importantly, using the state vector class and operator overloading,
the function that performs the fourth-order RungeKutta (R-K4) integration
becomes a direct expression of the R-K4 algorithm, just as it was when I was
dealing with scalars.
void step(double &t, State_Vector &x, State_Vector &x_dot,
const double h){
static State_Vector k1, k2, k3, k4;
k1 = h * x_dot;
k2 = h * deriv(t + h/2.0, x + k1/2.0);
k3 = h * deriv(t + h/2.0, x + k2/2.0);
k4 = h * deriv(t + h,
x + k3);
t += h;
x += (k1 + 2.0*k2 + 2.0*k3 + k4)/6.0;
x_dot = deriv(t, x);
}
Here Ive declared the local variables k1 to k4 as static. This avoids the
overhead of thrashing the memory management support and of creating and
deleting them as temporaries.
One moment to compare the above version of step() with the that in
Listing 12.8 should convince you of the value of operator overloading. Not
only have I eliminated the for loops, but also the temporary variable and
the code to compute it. Because of operator overloading, you can write algebraic expressions involving state vectors and the temporary values are hidden from you. This simplicity will pay off in spades later when I add
automatic step size control.
409
410
Notice again how the parameter lists for the procedures are reduced to
almost nothing. In some cases, Ive kept a parameter because the routine is
not always called with the same value as for the internal parameter; for
example, with the step size. Note also the default values in the constructor,
which allow you to set only those parameters you really care about.
How does this approach simplify main()? See it below in its entirety.
void main(){
Integrator I(State_Vector(1.0, 0.0));
I.integrate(3);
}
411
here will suffice. The code for the rest of the package is shown in Listing
12.10. (Discussion continues on page 415.)
412
413
414
415
416
3
3
1
1
1
k 3 = hf x + --- k 1 + --- k 2, t + --- h
6
3
6
[12.34]
1
3
1
k 4 = hf x + --- k 1 + --- k 3, t + --- h
8
8
2
1
3
k 5 = hf x + --- k 1 --- k 3, t + h
2
2
1
x ( t + h ) = --- ( k 1 + 4k 4 + k 5 )
6
1
e = ------ ( 2k 1 9k 3 + 8k 4 k 5 )
30
See how easy this is? If you doubt the value of using operator overloading, try coding this same routine using for loops and intermediate variables.
417
The problem that youre faced with now, though, is how do deal with the
error estimate. The first thing you must recognize is that the format of
step() is wrong. In general, you cannot assume that youll want to accept
the step, because having computed it, you may find that the error was too
large. No matter how tempting it is, you must never take a step that would
lead to an error out of range, so you cant let this function actually update
the state until youre satisfied that the step was legit.
Fortunately, this problem is easily fixed. Simply let step() compute the
increment in x, and decide externally whether the step was suitable or not.
void Integrator::step(double h){
k1 = h * x_dot;
k2 = h * deriv(t + h/3.0, x + k1/3.0);
k3 = h * deriv(t + h/3.0, x + k1/6.0 + k2/6.0);
k4 = h * deriv(t + h/2.0, x + k1/8.0 + (3.0/8.0)*k3);
k5 = h * deriv(t + h, x + k1/2.0 - 1.5 * k3 + 2.0 * k4);
e = (2.0*k1 - 9.0*k3 + 8.0*k4 - k5)/30.0;
dx = (k1 + 4.0*k4 + k5)/6.0;
}
The variables e and dx are new state vector variables declared in the body
of the class. Function step() is, in turn, called from a new function called
good_step(). The idea is that good_step() will never accept a step that is
not accurate, within some definition of that term. To keep from breaking the
integrator, you can define a temporary, stub version of this new function.
This stub version always accepts the step.
void Integrator::good_step(double h){
step(h);
t += h;
x += dx;
x_dot = deriv(t, x);
}
The error, e, is a vector estimate of the error incurred in the state vector
in a single step.
418
e1 + e2 + + e N
However, recall that the elements of e are (hopefully) very small. For that
reason, Ive had trouble in the past with floating-point underflow. Perhaps
more importantly, the square root function is going to burn some unnecessary
419
clock cycles. For this reason, I prefer to simply use the largest element of e as
the returned value. In effect, think of the elements of e as defining a hyperrectangle, rather than a hypersphere.
For most practical problems, the elements of the state vector have units,
which are almost certainly not the same for all. Some may be quite a bit
larger, in numerical magnitude, than others. So you should also normalize
the error value by the magnitude of the variable associated with it. If, for
example, one of the elements of the state vector is itself a vector perhaps
position a reasonable return value might be
[12.36]
ex
error = ------.
x
Again, youre tempted to use the usual vector definition of the magnitude, as the square root of the sum of the squares. But then you are back to
square roots again. Also, the same solution presents itself again. Use the
largest element in magnitude.
You now have a potentially serious problem. What if |x| = 0? Things are
then going to get nasty. But theres an easy way around this problem, also:
Limit |x| to be at least one. In effect, use relative error when |x| is large,
absolute error when it is small. Using a little pseudomath, you can write
[12.37]
max( e i )
.
error = ----------------------------------------max(max( x i , 1))
420
Notice the for loop, which bails out with an error message if the step size is
halved too many times. In general, its not a good idea to bail out of a general-
421
422
423
424
425
426
427
428
429
bogus computation for the updated state. That bogus computation may not
cause a problem, but more likely, it will introduce an error that compounds
itself as the run continues.
Even though you can force a good variable-step method to work through
discontinuities, to do so borders on criminal negligence. Strictly speaking,
you should never try to integrate across such discontinuities; rather, you
should break the solution into parts that are piecewise continuous. There is
only one reasonable and mathematically correct way to deal with discontinuities: stop the integrator, apply the impulsive change, and restart. Thats
why all my production integrators have the capability to restart at the previous state.
The process of stopping the integration is easier said than done, however.
The trick is finding out just where the discontinuities occur. In the case of a
rocket staging, the problem is easy, because the staging probably occurs at a
specific and predefined time. For cases such as this, you can simply program
the solution in two or more calls to the integrator.
Hitting walls is much more difficult, because you cant predict when in
time youll hit them. In my quad2 series, I solved this problem by defining
some function of the state that goes to zero at the boundaries:
[12.38]
f(x, t) = 0.
This function, which can be anything from one (which, of course, never
crosses zero and thus never triggers an action) to as complicated and nonlinear
a function as you choose, is computed by yet another user-definable function to
the routine called term(). I added code to the integrator to search for places
where the function passes through zero (thus changing sign). When the integrator sees a sign change, it backs up and iterates to find the precise time of the
zero crossing. This scheme turned out to be extremely useful, and its become a
fixture, almost a trademark, in my FORTRAN integrators. Its worked beautifully.
Unfortunately, the implementation details are a bit much for this chapter;
the math is rather complicated, and I havent talked yet about methods for
finding the zeroes of a function, for which I have a robust, second-order
method. To be frank, I have not incorporated it into quad2, where I still use
linear interpolation, which works well because the step size control presumably keeps things from changing too much in a single step.
430
Multiple Boundaries
The notion of terminating on a zero of a function is nice, but in general,
youre likely to have more than one edge condition or boundary to watch
for. A ball in a box, for example, has at least six such conditions and more if
you count corners. So how do you deal with multiple boundaries using only
a single scalar function? If you have multiple edge conditions, define multiple functions and create a single function by multiply them together. Then,
f(x) will go to zero when any one of the functions does. Ive used this
approach quite often with quad2, and it works very nicely.
Theres only one problem. Once the integrator returns, having found an
edge condition, you have no idea which one it found. This means that you
must provide code to find out which wall you hit. Remember, the integrator
has terminated, so its up to the main program to decide what to do before it
restarts the solution.
The problem is complicated by floating-point arithmetic, which means
that you may not be exactly on the edge when the integrator stops. You
might be at t = 9.999999999 instead of 10. Although quad2 is quite useful, I
found that searching for the reason it stopped was sometimes tricky, and the
complexity compounds factorially as the number of such conditions
increases.
Thats when it occurred to me that the one entity that best knew why
quad2 stopped would be quad2 itself. I just needed it to tell me why it
stopped. Unfortunately, quad2 didnt know either, because information
about which of the multiple boundary functions [multiplied together inside
term()] was responsible was obscured by that multiplication.
The answer to this problem is to return a vector function f(x, t). The integrator could stop when any element of this function crossed zero, and it
could tell which element it was.
Enter quad3, which does exactly that. In addition to monitoring all the
elements of the vector function f(x, t), it returns a vector of Boolean flags
telling me which condition(s) had been met. That solves the problem for
good.
If you think the logic for finding a single edge condition inside an integrator is tricky, you should see the logic for the vector case. There is no way I
can go into such detail here; its far outside the scope of this chapter, and I
have to hold something back for my paying customers!
431
1
1
k 2 = hf t + --- h, x + --- k 1
2
2
Translating into English, you are asking the derivative function the
question, If I have a state vector x of so-and-so at such-and-such a time,
what would my derivative vector be? In general, the trial state vectors used
in the R-K method are fictitious values. In the final solution, the state vector
will never actually take on these values.
Asking questions like this are appropriate when the derivative function is
known analytically and can be evaluated for any possible input value,
including times that havent occurred yet. I think you can see, though, that
such questions are completely inappropriate for a real-time system. At a
given time, the state vector is what it is, and you can do nothing to change
it. More importantly, you cant ask the system what the derivatives will
be at some future time. At least some of the derivatives are likely to come
from sensors like flow meters or accelerometers, and you have no idea what
their readings will be until youre actually at the time point in question. The
R-K method depends on knowledge of future values that you simply never
can have in a real-time system. In real time, you absolutely must restrict
yourself to values of the derivatives when they actually occur.
432
x = hf ( t, x ) .
If you denote your current value of the state vector as xi, the next value is
[12.41]
x i + 1 = x i + hf ( x, t ) .
Now the code for updating the state vector is a real-time integrator in one line.
x += h*deriv(t, x);
What value do you use for the step size, h, and how do you control it? In
almost every case, the answer is to evaluate the derivatives at a fixed time
interval; therefore, h is a constant. You must provide a periodic interrupt
that causes the code execute. On a PC, most people do this by making use of
the 18.2Hz real-time clock built into the PC hardware. Users can hook into
this clock by providing a user-supplied interrupt handler for INT 1Ch. In a
real-time system, getting such interrupts is not a problem. Almost any realtime system worthy of the name will have at least one task that executes at a
specific frequency driven by a (usually nonmaskable) external clock.
In either case, just set the step size equal to the period of the fixed frequency, insert the code shown above for updating the state vector into the
433
interrupt handler, and youve got yourself a real-time integrator. Youll find
this approach or one very much like it in almost all PC-based simulators.
Notice that its not absolutely necessary that the update rate be a fixed
frequency. As an alternative, you could simply put the update code into an
infinite loop and measure the time between successive passes to determine
the step size. To do this, though, you must have a clock with sufficiently
high resolution so that the step size can be measured accurately. Most computers arent equipped to give this kind of information, so the fixed-step
approach is almost universal. For all practical purposes, its also the only
approach that can be extended to higher orders.
For the step size, a good rule of thumb is to provide at least 100 integration steps per cycle of the highest frequency involved. In other words, if your
system must respond to disturbances of 10Hz, for example, your update frequency should be at least 1,000Hz. At that rate, dont expect the kind of
accuracy youre used to getting out of computers. Figure 12.4 shows a graph
of the solution of the standard test case
[12.42]
x = 2y
y = 2x.
where Ive taken 128 steps per cycle. At the end, the x-coordinate is in error
by more than 15 percent. The error is due to truncation error in the firstorder algorithm and is typical of what you can expect of this method. By
contrast, you saw earlier that 64 steps of R-K4 gave essentially perfect
results. You can improve the accuracy by using more steps: 8 percent error
for 256 steps, 4 percent error for 512 steps, 2 percent error for 1,024 steps,
1 percent error for 2,048 steps, and so on. If you want accuracy better than
1 percent, youd better figure on adding to the update frequency by at least
another factor of 10. This crude first-order method will never give you the
kind of perfection that youre used to from other computer math applications. Thats just a fact of life. Consider that if youre using the 18.2Hz PC
clock to set the update frequency, youd better not be modeling frequencies
in the system that are higher than 0.2Hz (five seconds per cycle). That
should give you pause to think if youre implementing a fast-moving jet
plane simulation.
434
Higher Orders
Although the first-order method may be perfectly acceptable for systems
controlled by feedback, plenty of situations where accurate solutions are
435
imperative dont have that luxury. One example is navigation, in which case
youre strictly dependent on the data received from the navigation sensors,
and any error incurred is going to propagate into an error in final position.
Clearly, for problems of this type, a higher order integration formula is
needed. However, Ive already noted that the higher order integration formulas based on the RungeKutta method wont work because they require
knowledge of the future that simply isnt available in real time.
At first glance, it seems that theres nothing you can do to solve this problem. Remember, though, that although its not fair to look at future values
of the derivatives, nothing says you cant remember past values, and that
notion is the key to higher order integrations in real time.
[12.43]
5 2 3 3 251 4
x i + 1 = x i + h 1 + ---- + ------ + --- + ---------
8
720
2 12
95 5 19, 087 6
+ --------- + ------------------ + f i
288
60, 480
The important thing about the backward difference operator for my purposes is strongly hinted at by its name. It depends only on past values of
events, not future values, which is just the kind of thing you need for a realtime system.
For some reason, most programmers dont like to deal with differences
directly, but rather with past values of the derivatives. In Chapter 10, I gave
the resulting formulas in terms of z-transforms. They are reproduced in
Table 12.1 in somewhat different form. The first of these equations is identical to Equation [12.40]. For completeness, Ive shown formulas through
order six, although Id be amazed if you ever need accuracy in real-time
problems beyond that given by fourth order.
436
Table 12.1
Adams formulas.
First order
xi + 1 = xi + h f i
Second order
h
x i + 1 = x i + --- ( 3 f i f i 1 )
2
Third order
h
x i + 1 = x i + ------ ( 23 f i 16 f i 1 + 5 f i 2 )
12
Fourth order
h
x i + 1 = x i + ------ ( 55 f i 59 f i 1 + 37 f i 2 9 f i 3 )
24
Fifth order
h
x i + 1 = x i + --------- ( 1, 901 f i 2, 774 f i 2 + 2, 616 f i 2 1, 274 f i 3 + 251 f i 4 )
720
Sixth order
h
x i + 1 = x i + --------------- ( 4, 277 f i 7, 923 f i 1 + 9, 982 f i 2 7, 298 f i 3
1, 440
+ 2, 877 f i 4 475 f i 5 )
Doing It
To show how its done, Ill implement the Adams algorithm for the fourth
order. Ill need past values of the derivatives, so instead of a single vector
x_dot, as I had in the R-K integrator, Ill define four values.
State_Vector f0,f1,f2,f3;
437
The code to update the state vector now must also rearrange the past values,
shifting them by one position.
f3 =
f2 =
f1 =
f0 =
x +=
f2;
f1;
f0;
deriv(t, x);
h*(55.0*f0 - 59.0*f1 + 37.0*f2 -9.0*f3)/24.0;
Figure 12.5 shows the results, for a step size of 64 steps per cycle (the
same as that used for R-K4). As you can see, the results are much more
accurate than the first-order method, as you might expect.
That funny little wiggle at the beginning of the graph is caused by the
Achilles heel of multistep methods: they not only use but require past values; in this case, the values of the derivative function represented by f1, f2,
and f3. Unfortunately, at the beginning of the run, I have no idea what those
values should be, so I just set them to zero. Note that, with these initial
438
This is clearly wrong. The step is actually 230 percent too large, but as you
can see, things soon sort themselves out as proper values are shifted into the
slots. After three steps, the function is up to full strength.
Ive seen a number of attempts made to avoid the wiggle at the beginning. The most obvious approach is to change the order of the integration
method: first order for the first step, second order for the second step, and
so on. This does indeed avoid the problem, but it doesnt (and cant) avoid
the fact that you dont have enough information to take a proper fourthorder step. Any error incurred in that first step, because only a first-order
method is used, is there for good.
Fortunately, it doesnt matter in most cases how you handle the initial
steps. Real-time systems tend to start up in very specific ways: airplanes are
sitting on the tarmac, ships are tied up at the dock. In such cases, zero is a
very good guess for the initial values of the derivatives. In any case, a few
milliseconds after startup, the startup transient is long gone, so the whole
thing turns out to be a nonissue in practice, all of which is a very good
argument for doing as little as possible in the way of special startup practices. Just make sure to set those initial values to zero. Never leave them
uninitialized.
The Coefficients
In both the formulas and the code, Ive shown the coefficients as rational
numbers, that is, the ratio of two integers. Thats fine for illustrative purposes, but in practice you should compute the coefficients to avoid the division. The reason is not so much for speed, as for accuracy. Assuming that
youre using integer arithmetic, as most real-time programmers do, you
cant afford to multiply by constants like 59, because youd be giving up six
bits of precision. I know that youre every bit as capable as I am of dividing
two integers and writing down the resulting coefficient, but I will show
them to you (Table 12.2) to save you the trouble.
Table 12.2
439
Adams coefficients.
First order
Second order
Third order
Fourth order
Fifth order
Sixth order
1.000000000
1.500000000
0.500000000
1.916666667
1.333333333
0.416666667
2.291666667
2.458333333
1.541666667
0.375000000
2.640277778
3.852777778
3.633333333
1.769444444
0.348611111
2.970138889
5.502083333
6.931944444
5.068055556
1.997916667
0.329861111
440
Doing it by Differences
I must tell you that every real-time implementation of the Adams method
that Ive seen was done using the formulas of Table 12.1. I must also tell you
that this is not the best way to do it because coefficients have values greater
than one and alternate in sign. Thats the price you pay for wanting to deal
only with past values, rather than differences. The original Adams formula
(Equation [12.43]) has no such limitation. All the coefficients in that formula are well behaved. Whats more, you no longer need different values of
the coefficients for the different orders; you only need to add one more term
to increase the order. In practice, the differences get rapidly smaller with
higher orders, so instead of subtracting nearly equal numbers, you end up
adding very small ones, which is a much better approach.
To round out this study, Ill implement the Adams formula directly.
State_Vector x,f,df,d2f,d3f, temp1, temp2;
x += h*(f + df/2.0 +5.0*d2f/12.0 + 3.0*d3f/8.0);
t += h;
temp1 = f;
f = deriv(t, x);
temp2 = df;
441
df = f - temp1;
temp1 = d2f;
d2f = df - temp2;
d3f = d2f - temp1;
Now, I think you can see why most implementers prefer the past-values
method rather than the difference table method: the computation of the differences is awkward and requires the use of temporary variables. But look
at the resulting graph (Figure 12.6). The initial dipsy-doodle is gone! The
reason is clear when you look at the formula. With all the differences ( df,
d2f, d3f) initialized to zero and no special effort on my part, the formula
reverts to the familiar first-order formula. Similarly for the next step, the
formula is equivalent to the second-order formula using past values, and so
on. In other words, I get the variable-order startup I was seeking, with no
extra work. Another powerful argument for the difference approach is seen
if you look at the maximum values of the differences.
df = 0.61685
d2f = 0.0608014
d3f = 0.005959
As promised, these values get smaller at higher orders, so any errors made in
multiplying them by their coefficients become less critical. Dealing with all
the temporary values is a bother and can cost computer time, but the
improvements in accuracy and startup behavior seem worth the price.
Again, using reference counts for the assignment operation should minimize
the cost in computer time.
442
No Forward References?
Throughout this chapter youve heard me say that forward references, that
is, references to points ahead of the current point, are not allowed. In one
special case, though, you can actually get away with it. This is the important
case in which you deal with second, rather than first, derivatives. I showed
earlier (Equation [12.6]) how a second-order differential equation such as
Newtons second law can be converted to two first-order equations.
Although the statement that you cant look ahead at future values to
determine the new state is still true enough, in this case, you can seem to
look forward because the two components, r and v, can be decoupled.
Instead of integrating the entire state vector at once, first integrate only the v
portion using the Adams formula in the normal manner. At this point, you
have v at the new time, vi+1. In effect, you have looked ahead. This means
you can use the more stable and accurate Moulton corrector formula to
update r.
Wrapping Up
[12.44]
443
19 4
3 5
x i + 1 = x i + h 1 ---- ------ ------ --------- ---------
160
2 12 24 729
863
6
------------------ f i + 1
60, 480
If your differential equations are second order, as many are (virtually all
in dynamic simulations), you can find no better or more accurate method
for integrating them in real time that the combination of Equation [12.43]
for the velocity and Equation [12.44] for the position. Even if the equations
of motion dont give the position and velocity in simple Cartesian form, you
can often define velocity-like variables that can be expressed in terms of
the derivatives of position-like variables, which still allows you to use the
same method. Heres a useful example based on second-order formulas that
I originally gave in Equations [10.58] and [10.59].
[12.45]
h
v i + 1 = v i + --- ( 3 f i f i 1 )
2
h
x i + 1 = x i + --- ( v i + 1 + v i )
2
Let me reemphasize the point that if you can split the state vector into
the form of Equation [12.6], Equation [12.45] is the best possible secondorder algorithm you will ever find. Because it achieves better performance
than any other second-order method and much better performance than any
first-order method with only a modest increase in complexity, it represents
perhaps the most promising compromise between speed and accuracy.
Remember it, and use it wisely.
Wrapping Up
I have at last reached the end of my study of numerical integration formulas
and methods. I hope you got some good out of them and learned something
you will be able to use in your future projects.
444
A
Appendix A
445
446
#ifndef CONSTANT_H
#define CONSTANT_H
extern const double pi;
extern const double halfpi;
extern double pi_over_four;
extern double pi_over_six;
extern const double twopi;
extern const double radians_per_degree;
extern const double degrees_per_radian;
extern const double root_2;
extern const double sin_45;
extern double sin_30;
extern double cos_30;
extern double BIG_FLOAT;
#endif
If you
Jmath.h
447
#include <math.h>
double pi = 4.0 * atan(1.0);
double halfpi = pi / 2.0;
double pi_over_four = pi / 4.0;
double pi_over_six = pi / 6.0;
double twopi
= 2.0 * pi;
448
#define min(x,y)
#define abs(x)
(((x) < 0
) ? -(x): (x))
#define sign(x,y)
(((y) < 0
) ? (-(abs(x))): (abs(x)))
Jmath.cpp
449
#define min(x,y)
#define abs(x)
(((x) < 0
) ? -(x): (x))
#define sign(x,y)
(((y) < 0
) ? (-(abs(x))): (abs(x)))
450
Jmath.cpp
// Safe square root
double square_root(double x){
if(x <= 0) return 0;
return sqrt(x);
}
// safe tangent
double tangent(double x){
x = ang_180(x);
if(abs(x) == halfpi)
return BIG_FLOAT;
return tan(x);
}
// Safe inverse sine
double arcsin(double x){
if(x>1)
return halfpi;
if(x<-1)
return -halfpi;
return asin(x);
}
// Safe inverse cosine
double arccos(double x){
if(x>1)
return 0;
if(x<-1)
return pi;
return acos(x);
}
451
452
// Inverse sine
double arcsin(double x){
if(abs(x) >= 1.0)
return sign(halfpi, x);
return atan(x/sqrt(1.0 - x*x));
}
// Inverse cosine
double arccos(double x){
return(halfpi - Arcsin(x));
}
// Four-quadrant arctan
double arctan2(double y, double x){
double retval;
if(x == 0)
retval = halfpi;
else
retval = atan(abs(y/x));
if(x < 0)
retval = pi - retval;
if(y < 0)
retval = -retval;
return retval;
}
*/
453
454
for segmentation
double b = pi/6;
double k = tan(b);
double b0 = pi/12;
double k0 = tan(b0);
455
456
Index
Symbols
#define 5, 8, 12
#ifndef 8
#undef 22
& 407
Numerics
1/x 147
8080 134
A
abs() 19, 2122
acceleration 239, 245246
of gravity 240
units of 241
accelerator 239
Ada xvi, 35
compilers 35
exception 35
functions 9
package 9
programming language 9
tasks 9
Adams
coefficients 439
formulas 436, 440
fourth-order 437
method 435
predictor formula 316317
AdamsMoulton 319, 321322, 325, 328,
334, 355, 373, 392, 435
algorithms
fourth-order 348
analog-to-digital (A/D) converter 292293
angle conversion 11
angles
degrees and radians 11
antilogs 182, 191192
Apollo 359
457
458
Index
approximations 269
linear 164
order of 271, 276, 281
to x 46
arccosine function 128
arcsine function 128
arctangent 35
See Chapter 6
error function 145
rational approximations for 179, 453
series 131
array
declaration in Fortran 401
artillery firing tables 358
asin() 37
assembly language 43
translation of 215
assembly programming language 5
assignment statements 6
asymptotic 47
atan2() 3839
B
Babbage, Charles 93, 358, 368
backward difference operator 282, 287,
297298
base 190
base 2 191, 198, 230231
BASIC
ON condition mechanism 35
belt and suspenders 28
Binary Angular Measure (BAM) 113
Bitlog 218, 232
definition of 215
Borland compiler 22
Borland Object Pascal 9
units 9
Borland Turbo C++ 362
boundaries
multiple 430
boundary condition 334
C
C data type 18
long double 18
C header file 4
C initializer 12
C library functions
conventions 17
C preprocessor 8, 12
macros 18
C programming language 78
C++ class constructor 12
C++ exception 35
C++ objects 12
C++ programming language 12, 14
C++ templates 31
calculators 9293
Hewlett-Packard 101
calculus
See Chapter 8
as source of power series 92
invention of by Newton 242
cannon balls 333
central difference operator 283, 286287,
291, 298
Index
closed-form solutions 357
CodeView 21
coefficients 116, 166, 168, 170171
for integer algorithm 116
integer 115
column matrix 402
compiler optimization 5
computed constants 7
conformant arrays 18
language support for 400
const 407
constant.cpp 36, 446
constant.h 8, 447
constants
computed 7
definition 3
floating point 3
global 9
initialization of 6
literal 4
named 7
numeric 3
continued fraction 139142
error 140
for constants 141
truncation of 142, 178
continuous vs. discrete systems 288
control system 358, 362, 371372, 428, 434
convergence 54, 56
converges 46
correctors 317318
cosine function 35, 94
definition 90
power series of 94
Crenshaw method 343
curve
area under as definition of integral 243
methods for estimatating the area under
245
459
D
D operator 292
data types
mixing 14
definite integral 244, 265
evaluating 244
degrees of freedom 383, 405
delta 250
notation 247, 250
deriv() 365, 373375, 386387, 405407,
428
derivative 252255, 263, 291
estimated 296
derivative function f(t, x) 360
Difference Engine 93
difference table 295, 297, 300, 322, 327
differences
divided 267
differential equation 292, 313314, 319
differentiation 253, 276, 293
numerical 276
disassembling 217
discontinuities 427
discrete vs. continuous systems 288
divided differences 267
dot notation 356, 359
double-angle formula 147
double-precision float 20
dynamic parameter 244
dynamic simulations 428, 443
See Chapter 12
discontinuities 427
dynamic system 314, 319, 322
460
Index
E
electron
mass of 183
embedded systems programming 35
Embedded Systems Programming magazine xiv, 234, 298
equations of motion 328, 334, 355
of a dynamic system 314
error
absolute vs relative 418
control 319, 349350
-handling mechanisms 28
propagation 322
error curve 144145, 150, 152
error_size 421
F
fabs() 19
fabsl() 19
floating-point 218
constants 3, 5
representation 51
floating-point numbers 14, 199
accuracy of 50
exponent of 51
formats
features of 49
mantissa of 51
normalizing 50
phantom bit of 50
sign of 51
split on convention 51
flow field 337338
formulas
fourth-order 349
FORTRAN xvixvii, xix, 58, 191, 211,
368370, 380, 384, 390391, 400
403, 409
COMMON area 6
variable naming convention 20
FORTRAN IV 7, 403
Block Data 7
variable dimensions 403
forward difference operator 282, 287, 297,
324
four-quadrant arctangent function 38
fourth order 347348, 351352
Friden 73
algorithm 72
calculator 7778
function
and indefinite integrals 244
overloading 19, 29
function names
as parameters 374
fundamental functions 25
Index
G
Galileo Galilei 239, 241242
Gap (John Gapcynski) 7374
geometric mean 189
global constants 9
good_step() 417418, 420
gravity
acceleration of 240
H
hacking
the floating-point format 54
half-step method 350
halting criterion 363
harmonic
oscillator 385
series 94
hearing
sense of 225
high-pass filter 237
Horners method 101, 103
Hornerize 117
Huen 342343
hyperbolic function 32
I
IEEE (Institue of Electrical and Electronic
Engineers) 231
standard 52
indefinite integral 244, 258, 260, 265
infinite series 92, 94, 128, 137
convergence of 95
truncation of 94
information hiding 6, 373
initial condition 313314, 334
instability 276
461
integer
algorithm 116, 120
arithmetic 4344
exponential function 232
representation
scaled 113
sine function 115
versions 112
integral 242, 263, 291
of a function 305
integral and derivative
relation between 265
integrate() 373, 377, 395396, 409
integration
numerical 305, 309, 313
rectangular 271
Runge-Kutta 309
trapezoidal 271
integration step size 362, 388, 415
Intel 80486 215
Intel math coprocessor 14
interpolation 120, 125, 265, 280281, 323
324
linear 120
quadratic 125
inverse functions 128
irrational numbers 141
iteration 46, 87
importance of initial guess 49
J
Jmath.cpp 447
Jmath.h 449
462
Index
mantissa 5051
Maritime Commissions ship simulator 371
math coprocessors 18
L
labs() 19
math.h 19, 21
Mathcad 137, 143, 172, 303, 316
Mathematica 137, 143, 303, 316
Mathworks, Inc. 370
Matlab 303
matrices 382, 401403
max() 19, 2122
Mersons method 351, 353
methods
multistep 334
single-step 335
microcontroller 43
Microsoft Excel 250
Microsoft Flight Simulator 358
Microsoft Visual C/C++ 21
midpoint 342
algorithm 344
method 345
min() 19, 2122
minimax 151, 154, 166
modular programming 6
modularity 6
modulo function 23, 104, 126
Moulton corrector formula 317
multiple boundaries 430
multistep integrator 328
multistep methods 334, 355, 373, 392, 435,
437
N
named constants 7
NASA 7273, 313, 329
real-time simulations 329
NASAs Marshall Space Flight Center 354
Index
natural log 190192, 208
Newton, Isaac 68, 239, 356358, 368, 379,
381383, 442
O
object-orientation 406, 421
odometer 239
operator overloading 408, 416
in C++ 406
operators 253, 261, 284
delay 287
notation 284
shift 287
out() 366, 373375, 386, 394, 396, 405
overloaded function names 21
overloading 19
pi 4, 15, 155
PID controller 237, 372
pirad angles
examples of 114
pirads 113, 115
planets
motion of 242
planimeter 246
Plauger, P.J. xvi, xix
polynomials 119, 274, 281
Chebyshev 119, 166
order of 272
ratio of 135
position
units of 241
potentiometer
logarithmic 225
pow() 211
power 212
quad0 427
QUAD1/quad1 369370, 427429
quad2 427
QUAD3/quad3 370371, 427, 430
quadratic convergence 4849
quadratic interpolation 125
463
464
Index
R
radians 9394
Radio Shack TRS-80 136
range
limiting 147
range reduction 105107, 147, 149, 156
effects of 156
rational approximation
arctangent function 179, 453
generation from truncated continued
fraction 141
real-time 358, 371, 432, 435
computations 431
programs 28, 328330
errors in 26
simulations 358, 371372
rectangular integration 266, 271
reference counting 440
round-off error 47, 95, 267, 275
RPN (reverse Polish notation) 101
RungeKutta 309, 322, 327328, 353, 355,
371, 373, 376, 392, 427, 431
See Chapter 11
derivation of 336
fourth-order 336337
suitability for real-time 431
run-time library 9
S
scalar variable 334
scientific notation 182
second-order 356, 382
differential equation 442
differential equations 381
equations 356
self-starting 327
algorithm 326
methods 322
Shanks formula 348
sign (x, y) 22
Simpsons Rule 275, 306, 308309, 311
312, 333
error 309
Skylab 370
slope 247
by divided differences 251
definition of 247
estimation of 251
of curve 247
speed 239240, 247248
average 241
See also velocity
speedometer 239
spinning top 357
sqrt() 25, 29
square root 4345
algorithm 52
integer version of 64
Index
binary version 78
initial guess 44
the high school algorithm 75
square root function 28
negative arguments 25
standard C++ library 21
Standard Template Library (STL) 31
starting values 89
state vector 379384, 388, 404409, 411,
417420, 431432, 437
State_Vector 406, 410, 420, 440
stdlib.h 21
step size 267, 275, 306, 322, 329, 335336,
339, 346, 349, 351, 362363, 369
370, 390, 392393, 395, 406, 408,
415, 418, 420421, 428, 433
fixed 349
integration 415
variable 335336
step() 367, 373, 375377, 386, 390, 394
395, 408409, 417
stopping criteria 89
streamline 337, 356
successive approximation
method of 69
synthetic division 137, 143, 315316
T
table lookups 120, 125
entry numbers for 125
tangent function 32, 130
definition 129
Taylor series 281282, 288, 339340
term() 429
465
U
underflow 2728
V
v(t) = area under the curve g(t) 242
v(t) = integral of a(t) 245
variables 17
variable-step method 328
vector
as column matrix 402
integration 384
state 404
variable 334
velocity 246
volume control 225
W
World War II 358
466
Index
X
x(t) = area under the curve v(t) 242
x(t) = integral of v(t) 245
Z
z operator 303305
Z-8000 61
Zenos Paradox 46
z-transform 286, 334335, 339, 372, 435
Embedded
Embedded Information
Information
You
You Need
Need to
to Succeed
Succeed
Thinking Inside
the Box
Sign up for the free Embedded.com
email newsletter!
Every week you will receive a bulletin of
industry news, expert insights, and
event and product information.
Embedded designers and engineering
managers rely on Embedded.com to
stay on top of their industry and to
help them purchase products for their
designs. Our weekly email newsletter
will help keep you up-to-the-minute on
the latest developments in the
embedded community.
Go to
www.eetnetwork.com/register to
register for your FREE embedded email
newsletter!
Celebrating 15 Years
of Industry Leadership!
Sign up for your FREE subscription to
Embedded Systems Programming!
Embedded Systems Programming is the
industry magazine devoted to
microcontroller, embedded
microprocessor, DSP and SoC-based
development. For 15 years, our in-depth,
technical articles have focused on practical,
design information from the firmware and
www.embedded.com
EmbeddedSystems
P
EmbeddedSystems
H Filters
Simulation Engineering
by Jim Ledin
Designing with
FPGAs & CPLDs
by Bob Zeidman
e-mail: [email protected]
www.cmpbooks.com
Embedded Systems
Firmware Demystified
by Ed Sutter
Explore firmware development from coldboot to network-boot. You will implement
CPU-to-peripheral interfaces, flash drivers, a
flash file system, a TFTP client/server, and a
powerful command-line interface to your
embedded platform. Includes a crosscompilation toolset for 21 processors and source for an extensible firmware
development platform. CD-ROM included, 366pp,
ISBN 1-57820-099-7, $49.95
e-mail: [email protected]
www.cmpbooks.com
TCP/IP Lean
Web Servers for Embedded Systems
Second Edition
by Jeremy Bentham
Implement dynamic Web programming
techniques with this hands-on guide to
TCP/IP networking. You get source code
and fully-functional utilities for a simple
TCP/IP stack thats efficient to use in
embedded applications. This edition shows
the Web server porting to the PIC16F877 chip
as well as over an ethernet connection.
Includes a demonstration port running on Microchips PICDEM.net
demonstration board. CD-ROM included, 559pp, ISBN 1-57820-108-X,
$59.95
Practical Statecharts
in C/C++
Quantum Programming for
Embedded Systems
by Miro Samek
Efficiently code statecharts directly in
C/C++. You get a lightweight alternative to
CASE tools that permits you to model reactive systems with UML statecharts. Includes
complete code for your own applications,
several design patterns, and numerous executable examples that illustrate the inner workings of the statechart-based
framework. CD-ROM included, 265pp, ISBN 1-57820-110-1, $44.95
e-mail: [email protected]
www.cmpbooks.com
MicroC/OS-II
The Real-Time Kernel
Second Edition
by Jean J. Labrosse
Learn the inner workings of an RTOS! This
release of MicroC/OS adds documentation for
several important new features of the latest
version of the software, including new realtime services, floating points, and coding
conventions. It is a completely portable,
ROMable, preemptive real-time kernel.
Complete code is included for use in your own applications. Hardcover,
CD-ROM included, 606pp, ISBN 1-57820-103-9, $74.95
Embedded Systems
Building Blocks
Second Edition
by Jean J. Labrosse
You get microcontroller theory and
functional code modules that can be used
to create basic embedded system functions. Hands-on exercises that employ the
real-time system modules provided by the
author demonstrate the key concepts unique
to embedded systems and real-time kernels.
This second edition features a new chapter on PC Services.
Hardcover, CD-ROM included, 611pp, ISBN 0-87930-604-1, $69.95
e-mail: [email protected]
www.cmpbooks.com