Scientific Programming: Numeric, Symbolic, and Graphical Computing with Maxima [1 ed.] 1527511170, 9781527511170

This book offers an introduction to computer programming, numerical analysis, and other mathematical ideas that extend t

304 93 7MB

English Pages 562 [563] Year 2018

Report DMCA / Copyright

DOWNLOAD PDF FILE

Table of contents :
Dedication
Table of Contents
Foreword
1. An Introduction To Programming
2. Computation in Mathematics
3. Graphics and Visualization
4. Interpolation and Approximation
5. Numerical Integration
Epilogue
References
Index
Recommend Papers

Scientific Programming: Numeric, Symbolic, and Graphical Computing with Maxima [1 ed.]
 1527511170, 9781527511170

  • 0 0 0
  • Like this paper and download? You can publish your own PDF file online for free in a few minutes! Sign Up
File loading please wait...
Citation preview

Scientific Programming

Scientific Programming:

Numeric, Symbolic, and Graphical Computing with Maxima

By

Jorge Alberto Calvo

Scientific Programming: Numeric, Symbolic, and Graphical Computing with Maxima By Jorge Alberto Calvo This book first published 2018 Cambridge Scholars Publishing Lady Stephenson Library, Newcastle upon Tyne, NE6 2PA, UK British Library Cataloguing in Publication Data A catalogue record for this book is available from the British Library c 2018 by Jorge Alberto Calvo. Copyright  All rights for this book reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted, in any form or by any means, electronic, mechanical, photocopying, recording or otherwise, without the prior permission of the copyright owner. ISBN (10): 1-5275-1117-0 ISBN (13): 978-1-5275-1117-0 Cover Photograph: Antarctic Ice by Christine A. Wilkins (2011). The beautifully intricate structures formed by ice crystals off the Antarctic coastline serve as a reminder both of the order required when designing computer programs and of the infinite possibilities that result when these are organized by a well-disciplined method. All figures were rendered by the author using Maxima’s draw package and the tikz package for the LATEX typesetting language. The following photographs are included with the copyright owner’s permission: ◦ Lofting Ducks (page 361) by Mathew Emerick (2012). ◦ Oratory at Sunrise (page 455) by Tyler Neil Photography (2015).

To my father

Table of Contents

Foreword . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ix Chapter 1. An Introduction To Programming . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.1

Getting Started with Maxima . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3

1.2

Symbolic Computation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21

1.3

User-Defined Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41

1.4

Repetition and Iteration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61

1.5

Leibniz’s Series . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73

1.6

Archimedes’ Sequence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87

1.7

Computational Error . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101

Chapter 2. Computation in Mathematics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 2.1

Fibonacci’s Rabbits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119

2.2

The Babylonian Algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133

2.3

Detecting Prime Numbers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147

2.4

Lists and Other Data Structures . . . . . . . . . . . . . . . . . . . . . . . . . . . 161

2.5

Gaussian Elimination . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177

2.6

Partial Pivoting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .191

2.7

Binary Codes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207

2.8

Floating-Point Numbers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225

Chapter 3. Graphics and Visualization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241 3.1

Celestial Mechanics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243

3.2

An Epitaph for Archimedes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265

3.3

Rabbits Revisited . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283

3.4

Projectiles in Motion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 303

3.5

Fourier Likes It Hot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .317

3.6

Plotting Curves by Sampling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 329

3.7

Plotting Lines with Pixels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347

Chapter 4. Interpolation and Approximation . . . . . . . . . . . . . . . . . . . . . . . . . . 359 4.1

Lagrange’s Formula . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 361

4.2

Piecewise Linear Interpolation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375

4.3

Interpolation using Splines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 385

4.4

Smooth Splines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 401

viii

TABLE OF CONTENTS

4.5

Approximation using Splines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413

4.6

Interpolating Curves . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 427

4.7

Interpolating Surfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445

Chapter 5. Numerical Integration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 457 5.1

Riemann Sums . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 459

5.2

Trapezoids and Parabolas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 475

5.3

Smooth Splines Revisited . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 491

5.4

Gaussian Quadrature . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505

5.5

Monte Carlo Simulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 519

Epilogue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 533 References . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 537 Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 539

Foreword “If one approaches a problem with order and method, there should be no difficulty in solving it; none whatever.” — Hercule Poirot as quoted in Death in the Clouds by Agatha Christie (1935)

This book was developed for an undergraduate course in scientific programming, offering an introduction to computer programming, numerical analysis, and other mathematical ideas that extend the basic topics learned in calculus. It is designed for students who have finished their calculus sequence but have not yet taken a proofs course. The primary goal is to teach students how to write computer programs, and covers both the general building blocks of programming languages (such as conditional statements, recursion, and iteration) as well as a description of how these concepts are put together to allow computers to produce the results they do. In particular, careful attention is given to binary arithmetic, the IEEE standard for floating-point numbers, and algorithms for rendering graphics. The content builds on the topics covered in an introductory calculus course, including the numerical solution of both ordinary and partial differential equations, the smooth interpolation of discrete data, and the numerical approximation of non-elementary integrals. More importantly, however, this book will teach students about “order” and “method,” particularly when it comes to organizing data and solving problems. The guiding principle throughout is the belief that, for a novice mathematician, writing computer programs can be a useful exercise in developing the intuition for abstract concepts necessary to make the transition towards writing proofs. If nothing else, when you write an incorrect program, you get immediate feedback that something went wrong, even if the error message itself might

x

FOREWORD

seem cryptic at first. This gives you a chance to find mistakes in a way that writing an incorrect proof never can. All of the programming in this book is done in an open-source Computer Algebra System (CAS) called Maxima. There are several reasons for this choice of software. First of all, instead of tackling a high level language like C, Java, or Python, we prefer a CAS that provides students a friendly, unified front-end which allows them to perform more familiar mathematical tasks, such as graphing functions or solving equations, as well as write new programs. Secondly, in contrast to other well-known proprietary systems, Maxima is freely available for Macs, Windows, and Unix machines. Not only is Maxima supported by a large network of volunteer developers and used by researchers throughout the world, but it provides an excellent environment in which students can learn the basic structures of programming before moving on to other popular platforms. Unlike mastering a foreign language, which sometimes involves learning new and alien grammatical concepts like noun declensions (in Latin) or adjectival nouns (in Japanese), making the transition from one programming language to another often involves only small adjustments in syntax. The epilogue at the end of this book provides some simple examples of how this works in practice. To install Maxima on your computer, visit the Maxima Project’s website at maxima.sourceforge.net, download the installation files for your operating system, and follow the documentation included with the installer. For Unix and Windows machines, this involves running a single script. For Macs, more care is required as new fonts must be loaded and three different applications must be installed and configured to work together; for more detailed instructions, consult the excellent article at: themaximalist.org/about/my-mac-os-installation. Any additional questions can be directed to the Maxima discussion email list, which can be accessed from: maxima.sourceforge.net/maximalist.html. I am indebted to Michael Marsalli, who entrusted me with the development of this course, and to Patrick Kelly, Ricardo Rodriguez, and my other colleagues at Ave Maria University for their continual advice. My gratitude also goes to all of my Math 270 students who were subjected to one iteration of course notes after another; their

FOREWORD

xi

feedback, both implicit and explicit, has made this a better book than it otherwise would have been. My beautiful wife and children deserve a special word of mention since they have tolerated many a late night, especially as I put the manuscript in its final form. Finally, I wish to dedicate this book to my father, to whom I owe more than I could ever repay. I have many fond memories of going as a child to see his enormous Burroughs B1700 mainframe computer at work. The world has changed a lot since the time of punch cards and tape reels. His countless sacrifices throughout these years have made me into the man I am today. Framingham, Massachusetts March 2018

CHAPTER 1

An Introduction to Programming “Method, you comprehend! Method! Arrange your facts. Arrange your ideas. And if some little fact will not fit in—do not reject it but consider it closely. Though its significance escapes you, be sure that it is significant.” — Hercule Poirot as quoted in The Murder on the Links by Agatha Christie (1923)

1.1. Getting Started with Maxima In pure mathematics, we often study numbers, formulas, and geometrical shapes for their own sake, with no intention of ever finding an application for our newfound knowledge. For instance, in a previous calculus course, you may have learned that Archimedes of Syracuse (c. 287 – 212 BC) showed that the infinite sequence       √ √ √ √ 2 2, 4 2 − 2, 8 2 − 2 + 2, 16 2 − 2 + 2 + 2, . . . converges to π. You may have also learned that Gottfried Wilhelm Leibniz (1646 – 1716) proved that the infinite series 4 4 4 4 4 4 4 + − + − + − + ··· 3 5 7 9 11 13 15 is also equal to π. From a purely theoretical vantage point, these two statements stand side by side as equally valid facts. From a computational point of view, however, we may consider how each of these infinite limits may be used to provide a suitable approximation for the number π. For example, to determine a partial sum in Leibniz’s series, we simply add finitely many fractions together; if one finite sum is deemed inadequate, we can improve it by adding more fractions. Unfortunately, as we shall soon find out, Leibniz’s series converges painfully slowly, so it will take literally hundreds of additions before we arrive at a reasonable approximation for π. On the other hand, computing a sufficiently complicated term in Archimedes’ sequence involves working out one square root after another, starting from the innermost part of the formula and making our way to the outermost power of two. If we are dissatisfied with one approximation, then we need to start the next approximation essentially from scratch. Nevertheless, if we survive the computations, the convergence for this sequence is quite fast and provides accurate estimates of π in short order. This is essentially what Archimedes discovered some twenty two hundred years ago, when he determined that π ≈ 22 7 ≈ 3.14. It is all the more impressive to remember that he completed this excruciatingly delicate calculation entirely by hand and without the benefit of our decimal number system. The advent of the computer has put large-scale numerical experimentation within our reach. For instance, back in 1949, ENIAC (the first general-purpose computer) determined the first 2 037 places of π in about 90 hours; nowadays, a desktop computer can do the same 4−

4

1. AN INTRODUCTION TO PROGRAMMING

in just a couple of seconds. However, we are still faced with the task of explaining to our computers exactly how to perform the calculations that we need them to perform. This is where programming languages come into play. In the course of this book, we will start to learn a programming language called Maxima. Just as our everyday thoughts are expressed in English (or perhaps Latin or Spanish) and our descriptions of quantitative phenomena are expressed using mathematical notation, our computational instructions will be expressed in the language of Maxima. Before successfully using computers to study a mathematical problem, we need to understand three key areas: x the mathematical theory that underlies the problem at hand, y the implementation, or the way that we choose to represent mathematical objects inside the computer, and z the syntax, or the grammar that we use to communicate our instructions to the computer. For instance, in order to compare the computational efficacy of Leibniz’s series and Archimedes’ sequence in our discussion above, we need to remember the mathematical concepts of infinite sequences and series, as well as the definition of convergence. These definitions form part of the mathematical theory that gives shape and context to the problem. Next, we need to decide whether these abstract objects will be represented numerically, symbolically, or graphically in our computer. This is the realm of implementation. Finally, we need to know what instructions to type into our computer to bring about this implementation. This is a question of syntax; it is here that we shall start our study of programming in general and of Maxima specifically. In order to begin, you will need to sit down in front of a computer terminal and start up a session of Maxima.1 You can do this by clicking on the wxMaxima icon from the Applications folder in a Macintosh or the Start menu in a Windows PC. Depending on the version of the software installed in your computer, the wxMaxima icon will look something like this:

After you click on this icon, your computer will open up a new Maxima window for you. When the Maxima session first starts up, you will see 1 For instructions on how to install Maxima on your computer, consult page x.

1.1. GETTING STARTED WITH MAXIMA

5

the words “Ready for user input” at the bottom of the window. This is Maxima’s way of telling you that she is ready for your instructions. Maxima consists of three distinct components running simultaneously on your computer: x an external user interface or “front end” called the iris, y an internal computational engine called the kernel , and z an extensive library of functions bundled in packages. The Maxima window is just the visible part of the iris, which is run by the application wxMaxima. The kernel is maintained by a separate application that runs in the background of your computer. The various packages are stored in a hidden directory in your computer’s hard drive. For the sake of simplicity, we will use the term “Maxima session” to refer to our interaction with all three of these components. The main content of a Maxima session is organized as a sequence of cells, each one of which is made up of some text or of some Maxima instructions and their corresponding results. Every time that you type an instruction into the iris, hold down the Shift key on your keyboard, and press Enter, the iris passes this instruction to the kernel. The kernel may need look up one or more functions from a package in the library to execute this instruction, but when it is finished, it passes the result back to the iris to display. Then the whole process is repeated with a new cell. To start, let us suppose that you want to make a header for your Maxima session. You may wish to include such information as your name, today’s date, a title, and whatever other relevant comments you think are appropriate. For example, if you were working on the homework problems at the end of this section, you could include a phrase like “Section 1.1 homework” as part of your header. To do this, select Cell  Insert Text Cell from the drop-down menu at the top of the screen. Immediately, a cell marker in the shape of a red square bracket will appear on the left hand side of the window. You can then start typing your header information. When you have finished, you can create a new cell by going to the drop-down menu and selecting Cell  Insert Input Cell Again, there will be a cell marker on the left-hand side of the window,

6

1. AN INTRODUCTION TO PROGRAMMING

but this time you will also see an arrow-shaped input prompt that looks like this: --> The simplest kind of expression you might type at the input prompt is a number: --> 485 When you press Shift-Enter, Maxima will respond by displaying the same number back to you: (%i1) 485 (%o1) 485 You will observe that the arrow prompt was replaced with the input label (%i1), and that the corresponding output is also given an output label (%o1). For the sake of simplicity, we will ignore input and output labels throughout this book. Instead, we will indicate Maxima input and output by using a cell marker, with the instructions given in typewriter font and the corresponding output in normal Roman font. Thus, the short interaction above would be rendered as follows: 485; 485 Take special note of the use of the semicolon in the expression above. This is our first encounter with Maxima’s syntax, which requires that every expression end in either a semicolon or a dollar sign. Ending an expression with a semicolon instructs Maxima to display the result of evaluating the expression. In our example above, this result was the number 485. In contrast, ending an expression with a dollar sign instructs Maxima not to display the result and to just move on to the next expression. If you do not have the appropriate punctuation mark, then Maxima will insert a semicolon automatically for you, but sometimes this can result in an error message. Numbers may be combined with arithmetic operators into more complicated expressions, such as: 139 + 346; 485

1.1. GETTING STARTED WITH MAXIMA

7

Table 1-1. Maxima’s five arithmetic operators, in order of decreasing precedence. operator

operation

associativity

^

exponentiation

right to left

/

division

left to right

*

multiplication

left to right

-

subtraction

left to right

+

addition

left to right

5 * 97; 485 Maxima recognizes the five arithmetic operators listed in Table 1-1. In addition, parentheses provide a straightforward way to create even more complex expressions out of simpler ones by means of nesting. For instance, you might enter: (3 * 5) / (10 - 6); 15 4 (3 * ((2 * 4) + (3 + 5))) + ((10 - 7) + 6); 57 Observe that as soon as you type an open parenthesis, Maxima will display it along with its matching close parenthesis, highlighting both. The cursor will then be placed in between the parentheses to allow you to enter a nested expression. When you reach the end of the expression, simply type a close parenthesis or use the right arrow key to move the cursor over the one that is already there. At first, you may find Maxima’s automatic parenthesis matching a little awkward and it might take you some time to get used to it. However, in time you will come to appreciate its utility, especially when you are entering much more complicated expressions than the ones above. In principle, there is no limit to the depth of nesting or to the overall complexity of the expressions that Maxima can evaluate. In

8

1. AN INTRODUCTION TO PROGRAMMING

fact, when presented with even the most complicated of expressions, Maxima always operates in the same basic read-evaluate-display cycle: x The iris reads an expression from the terminal. y The kernel evaluates the expression. z The iris displays the result and awaits a new input. When it comes to evaluating complex expressions, the second step in this cycle consists of first evaluating each subexpression separately, and then combining the results by applying the operator in question. For example, in order to evaluate the expression (3 * ((2 * 4) + (3 + 5))) + ((10 - 7) + 6), Maxima first evaluates each one of the subexpressions 3 * ((2 * 4) + (3 + 5))

and

(10 - 7) + 6

separately, and once it has those values in hand, it adds them together, as indicated by the operator +. Of course, to evaluate the first subexpression above, Maxima must evaluate 3

and

(2 * 4) + (3 + 5),

and to evaluate the second of these, Maxima must evaluate 2 * 4

and

3 + 5.

The process continues in this way until our original expression has been broken down into its simplest atomic constituents, at which point these are combined, two at a time, to make up the final result. Fig. 1-1 shows a tree-like schematic of this evaluation process. The computation starts at the node at the top of the tree and moves down the branch on the left side as the first subexpression is evaluated. When this value is determined to be 48, the process returns to the top and follows the branch on the right as it evaluates the second subexpression. Then, once this value is found to be 9, the two values are combined into the final result of 57. In practice, you can avoid using unnecessary parentheses in Maxima expressions. Therefore, instead of typing the long expression above, you might enter: 3 * (2 * 4 + 3 + 5) + 10 - 7 + 6; 57

1.1. GETTING STARTED WITH MAXIMA

9

+ 57

* 48

+ 9

+ 16

3

* 8

2

4

- 3

+ 8

3

10

6

7

5

Fig. 1-1. Evaluating (3*((2*4)+(3+5)))+((10-7)+6). In either case, Maxima will return precisely the same value. In the absence of any parentheses, complex expressions are evaluated by following a strict precedence rule in which higher precedence operators are applied before lower precedence ones. The five arithmetic operators that appear in Table 1-1 are listed from highest to lowest precedence. This means that, in an expression with no parentheses, exponentiation is done first, followed by division, multiplication, subtraction, and finally addition. If the same operator appears more than once in an expression, then it will typically be applied from left to right; this is known as left-to-right associativity . For instance, when evaluating the expression 3 * (2 * 4 + 3 + 5) + 10 - 7 + 6, Maxima will first tackle the highest precedence operation, which in this case happens to be the product 3 * (2 * 4 + 3 + 5).

10

1. AN INTRODUCTION TO PROGRAMMING

3 * (2 * 4 + 3 + 5) + 10 - 7 + 6 ↓ 3 * (8 + 3 + 5) + 10 - 7 + 6 ↓ 3 * (11 + 5) + 10 - 7 + 6 ↓ 3 * 16 + 10 - 7 + 6 ↓ 48 + 10 - 7 + 6 ↓ 48 + 3 + 6 ↓ 51 + 6 ↓ 57 Fig. 1-2. Evaluating 3*(2*4+3+5)+10-7+6. This is followed by a subtraction and two additions, which are performed from left to right. Of course, before even the first multiplication can take place, the subexpression 2 * 4 + 3 + 5 must be evaluated. This, too, consists of a multiplication followed by two additions. Fig. 1-2 gives a step-by-step schematic of the entire evaluation process. Observe that, even though the final result is the same as before, the order in which the individual subexpressions were evaluated and combined is, in fact, different than the one shown in Fig. 1-1. The only exception to the left-to-right associativity rule is exponentiation, which has right-to-left associativity , as indicated in the last column of Table 1-1. We can see this principle at work in an expression like 4^3^2; 262144

1.1. GETTING STARTED WITH MAXIMA

11

which gives the same answer as the nested expression 4^(3^2); 262144 rather than the much smaller value of the expression (4^3)^2; 4096 Besides the five arithmetic operators discussed above, Maxima is equipped with literally hundreds of primitive functions at our disposal. For example, we can call on the function sqrt(), which computes the square root of any number we give it: sqrt(45); √ 3 5 A function call, like the one above, is always composed of the name of the function followed by one or more inputs surrounded by a pair of parentheses. As before, the use of parentheses allows nesting of functions, or indeed any combination of functions and operators, inside one another. For instance, suppose that we ask Maxima to determine the value of the following compound expression: 2 + sqrt(5^2 + (3 * 4)^2); 15 In this case, Maxima first evaluates the subexpression 5^2 + (3 * 4)^2 resulting in a value of 169. This value is then passed as the input to sqrt(), which returns an output value of 13. Finally, this result is added to 2 to produce the final answer displayed above. Table 1-2 lists some useful primitive functions of which you should take note. As their names indicate, these functions compute absolute values, natural exponentials and logarithms, as well as the standard and inverse trigonometric functions. You should take particular care to remember that log() computes the natural logarithm (base e) and not the common logarithm (base 10), as you might otherwise expect: log(2.718281828459045); 1.0

12

1. AN INTRODUCTION TO PROGRAMMING

Table 1-2. Sixteen primitive functions of note.

abs()

exp()

log()

sqrt()

sin()

cos()

asin()

acos()

tan()

cot()

atan()

acot()

sec()

csc()

asec()

acsc()

You can store a computational object, like a number or a formula, in the kernel’s memory by using the variable assignment operator denoted by a colon (:). For instance, the command pi : 3.14; 3.14 makes the variable name pi indistinguishable from the numerical value 3.14 in any expression you might type. For example: pi; 3.14 2 * pi; 6.28 pi^2 - 0.14 * pi - 6.70172; 2.71828 2 * cos(pi/5); 1.618408361976065 You can even use the value of one variable when you assign values to other variables. For instance, you might enter: circumference : 2 * pi * radius; 6.28 radius

1.1. GETTING STARTED WITH MAXIMA

13

area : pi * radius^2; 3.14 radius2 This associates each of the variables circumference and area with a formula that depends on the variables pi and radius. In each case, pi was replaced with its value. On the other hand, radius, which is a free variable that has not yet been given a value, appears by name only. We can replace it with a value, say with the radius of the earth (in kilometers), by calling the substitution command subst() as follows: rad_of_earth : 6371; 6371 subst(radius = rad_of_earth, circumference); 40009.88 subst(radius = rad_of_earth, area); 1.2745147274 108 Notice that subst() takes two inputs separated by a comma. The first consists of the substitution we wish to make (using an equal sign rather than a colon), and the second is the expression in which we wish to make the substitution. The resulting substitution is only temporary and does not affect the values stored in memory: radius; radius circumference; 6.28 radius area; 3.14 radius2 As you might imagine, Maxima maintains a record of all of the variables assigned during a session. You can see the names in this list by evaluating the variable values: values; [pi, circumference, area, rad of earth]

14

1. AN INTRODUCTION TO PROGRAMMING

You can remove a variable from this list by invoking the command kill():2 kill(rad_of_earth); done As expected, this command removes the variable in question from the list of assigned variables and disassociates it from its former value: values; [pi, circumference, area] rad_of_earth; rad of earth Although you can use nearly any combination of letters or numerals and even some symbols like % or \ to name a variable, there are a few rules you must abide by. First of all, you cannot start a variable name with a numeral. Secondly, you should remember that Maxima is case-sensitive, so the names Billy, BILLY, and billy are all distinct. Finally, there are a few protected names that Maxima reserves for its own use. For example, you would get an error if you tried to store a value under the variable name values. You would get a similar error if you tried to assign a value to the names %e, %phi, or %pi, since these are permanently assigned to the exact values of the mathematical constants e, ϕ, and π. You can see their decimal (or floating-point) approximations by using the special function float() as follows: float(%e); 2.718281828459045 float(%phi); 1.618033988749895 float(%pi); 3.141592653589793

2 This aggressive-sounding command can also allow you to clear all of the contents in the kernel’s memory and reinitialize the Maxima session. Simply enter kill(all). Yikes!

1.1. GETTING STARTED WITH MAXIMA

15

Suppose that, inspired by the last result, you decide to use a better approximation for π in the circumference and area formulas that we defined above. To start, you would give the variable pi a new value by entering: pi : float(%pi); 3.141592653589793 This change will affect all future interactions with Maxima during this session. For example, you can now define a new formula for the volume of a sphere by typing: volume : 4/3 * pi * radius^3; 4.188790204786391 radius3 As expected, the variable pi in this formula was replaced by its (new and improved) numerical value. On the other hand, the circumference and area formulas are not affected by the change in the value of pi. Instead, they stubbornly cling to their old (and now obsolete) values: circumference; 6.28 radius area; 3.14 radius2 To understand the reason behind Maxima’s strange behavior here, we need to go back and remember exactly what happened in our interactions with Maxima, from the kernel’s point of view. When we first assigned formulas to circumference and area, the variable pi was already assigned the value 3.14. Therefore, these formulas were immediately evaluated, respectively, as 6.28 radius

and

3.14 radius2 .

These were the values that were originally stored in the kernel’s memory. When the free variable radius was replaced with a numerical value, Maxima was able to evaluate both circumference and area as numbers, but their values in memory (in other words, the formulas above) never changed. Since the variable name pi does not appear in either of those formulas, they remained unaffected when the value of pi was changed later on, as we saw above. These observations might be summarized in the following fortune cookie mantra:

16

1. AN INTRODUCTION TO PROGRAMMING

The kernel remembers your instructions only in the order that you enter them.

Suppose that, in an attempt to remedy the situation, you decide to move the cursor back to the cell where you first gave pi a value, and change the contents of that cell to: pi : float(%pi); 3.141592653589793 You can then bring the cursor back to bottom of the Maxima session and evaluate circumference and area once again. In this case, you will see: circumference; 6.28 radius area; 3.14 radius2 Note that nothing has changed in the kernel’s memory! Even though the iris shows the new definition of pi occurring before the definitions of circumference and area, as far as the kernel is concerned, pi was given its new value after these two formulas were defined. In fact, you can see that this is the case by taking a closer look at the numbers in the input and output labels for the cells containing the respective definitions. Once again, you will find that the kernel remembers your instructions only in the order that you enter them. In order to fix the two formulas in your computer’s memory, you will need to enter the original definitions for circumference and area a second time. Luckily, Maxima keeps track of all of the instructions that you have entered so far in your session, saving you the effort of typing the definitions from scratch. You can see these instructions by holding down the Alt key while pressing the up arrow key several times. You will see the last commands that you typed appear in reverse order in a new cell. You can also scroll forward through your commands by holding down the Alt key while pressing the down arrow key. For now, continue pressing Alt-⇑ until you see the command with which you first defined circumference, and then, without changing a thing, press Shift-Enter:

1.1. GETTING STARTED WITH MAXIMA

17

circumference : 2 * pi * radius; 6.283185307179586 radius Repeating the same process, press Alt-⇑ until you find the command with which you defined area. Once again, without changing anything, press Shift-Enter: area : pi * radius^2; 3.141592653589793 radius2 You have finally changed the contents of the kernel’s memory as desired! Using the mouse to move back and forth between the various cells in a Maxima session is a convenient way to correct small errors; however you should do so only with extreme caution. In particular, since the order in which the contents of the Maxima session appear in the iris might not reflect the true contents of the kernel’s memory, it is also an excellent way to propagate chaos, mayhem, and confusion. A safer (though arguably less efficient) strategy is to make use of the Alt-⇑ and Alt-⇓ shortcuts to scroll through your past commands and make the appropriate changes in order. Regardless of the approach you choose, you should always keep in mind that the kernel remembers your instructions only in the order that you enter them! “Nobody expects the Spanish Inquisition!”3 And nobody expects to encounter critical errors in their computations. However, every once in a while things may go awry and you will be required to restart Maxima. To prevent losing your data in such an event, you should save your work often. This can be done by selecting File  Save from the drop-down menu at the top of the screen, by clicking the Save icon at the top of the Maxima window, or by using the keyboard shortcut Command-S. The first time that you save a Maxima session, you will be asked to choose a name, a location for your file, and a document format. There are two file format options from which to choose. One of these options is a “wxMaxima document” ending with the file extension .wxm. This format saves all of your input during the session (including text comments), but none of the corresponding output produced by Maxima. 3 Monty Python’s Flying Circus, Series 2, Episode 2 (1970)

18

1. AN INTRODUCTION TO PROGRAMMING

To recover the output, you would have to evaluate all of the session’s input again, perhaps by choosing the drop-down menu selection Cell  Evaluate All Cells or by pressing Command-R. The second option is to save your Maxima session as a “wxMaxima xml document” ending in the extension .wxmx. This format saves both the input and output from a session, but at the cost of producing larger files. In general, the choice of format is entirely up to you, but if you are planning on turning in a Maxima session as a homework assignment, you should always use the xml format so your instructor can see the same results you saw before you saved your work. This is particularly important if you ignored our advice (which you are always free to do) and the contents of the iris do not reflect the order in which you evaluated them. Finally, to close the Maxima session and exit, you can either go to the drop-down menu and select wxMaxima  Quit wxMaxima or press Command-Q. If you have not already done so, you will be offered one last chance to save your work. Then, after a few moments, the iris will close down until you summon Maxima once again at a later time.

Exercises for Section 1.1 1. Add parentheses to the expression 3 + 2 * 10 - 1; so that it evaluates to each of the following values. Explain the evaluation process in each case. (a) 21

(b) 22

(c) 45

(d) 49

2. Add parentheses to the expression 3 * 8 - 4 / 2; so that it evaluates to each of the following values. Explain the evaluation process in each case. (a) 6

(b) 10

(c) 18

(d) 22

3. Remember that the function log() computes the natural logarithm. Explain how you can still use Maxima to compute logarithms with an arbitrary bases. In particular, use Maxima to determine the value of each of the following logarithms. Note that you might need to use the function float() to convince Maxima to give you a simplified answer. (a) log2

√  2 (b) log3 (81)

(c) log5 (0.04) (d) log10 (101)

4. According to the United Nations Population Fund (UNFPA), the world reached a population of 7 billion people in late 2011 or early 2012. (a) Suppose that the 149 million square kilometers of land on the surface of the earth was divided equally among this population, so that each person was allotted a square parcel of land. How long would the sides of each square parcel be? (b) According to the United Nations Food and Agriculture Organization (FAO), only about 32% of the surface of the earth is considered arable and suitable for farming. If only the arable land was divided into equal square parcels among the population, how long would the sides of each square parcel be? (c) Express your answers to parts (a) and (b) in terms of some concrete unit of length. For instance, you might compare them to the height of the Empire State Building, the span of

20

1. AN INTRODUCTION TO PROGRAMMING

the Indianapolis Motor Speedway, or the length of a football field. 5. In the third century BC, Eratosthenes of Cyrene (c. 276 – 195 BC) noted that the sun was directly overhead at noon on the first day of summer in the Egyptian city of Syene, on the Nile River near the Tropic of Cancer. At exactly the same time, in the northern city of Alexandria, on the Mediterranean coast, the sun was approximately 7.2◦ to the south. Supposing the earth to be a perfect sphere, Eratosthenes concluded that its circumference was given by the formula C=

360 d, 7.2

where d is the distance between Alexandria and Syene.

Syene

7.2◦ sunlight

Alexandria

(a) Explain why Eratosthenes’ formula is correct. If you choose, you may refer to the diagram above, but be sure to use complete sentences and proper grammar to formulate a wellreasoned explanation. (b) According to surveying records dating to the times of the Pharaohs, the distance from Alexandria to Syene was 5 000 stadia, a unit of measurement commonly used in antiquity. Assuming that this measurement is correct, what is the circumference of the earth in stadia? (c) Recent archaeological studies suggest that a stadion measures about 157.5 meters. In this case, what is the circumference of the earth in kilometers? Compare your answer to the result obtained from the circumference formula in this section.

1.2. Symbolic Computation Most of our interaction with Maxima thus far has consisted in combining numbers with arithmetic operations, modifying them with mathematical functions, and storing them in memory using the assignment operator. This sort of behavior, aptly described by the broad term of numerical computation, may lead you to the conclusion that Maxima is nothing more than a sophisticated calculator. But this is hardly the case. We already caught a glimpse of Maxima’s more advanced capabilities when we evaluated the square root function with an input that was not a perfect square: sqrt(45); √ 3 5 Instead of providing us with a decimal approximation, Maxima replied with a simpler but exact version of the number we wanted. Of course, we can ask Maxima to provide the corresponding numerical approximation with the command float(sqrt(45)); 6.708203932499369 The point is that Maxima allows us to manipulate exact mathematical expressions and only worry about finding decimal approximations as a last step, assuming that we even do this at all. Indeed, as we saw earlier, Maxima can manipulate free variables before we assign any values to them. For example, we can ask Maxima to evaluate expressions like x + x + x; 3x or x * x * x; x3 and she knows perfectly well how to do this, even though x does not have a well-defined value assigned to it. This type of interaction lies at the heart of what we mean by symbolic computation. Whenever possible, Maxima automatically performs standard simplifications like the ones above. However, many other possible simplifications (including logarithmic and trigonometric identities) are

22

1. AN INTRODUCTION TO PROGRAMMING

applied only when requested. For instance, Maxima allows equivalent expressions like the following to stand unmodified side by side: a/c + b/c; b a + c c (a+b)/c; b+a c log(x) + 2*log(y) - log(z); − log (z) + 2 log (y) + log (x) log(x*y^2/z);  2 xy log z sin(x + y); sin (y + x) sin(x)*cos(y) + cos(x)*sin(y); cos (x) sin (y) + sin (x) cos (y) On the other hand, we can ask Maxima to apply the appropriate identities to move back and forth between these expressions as follows: combine(a/c + b/c); b+a c distrib((a+b)/c); b a + c c logcontract(log(x) + 2*log(y) - log(z));  2 xy log z

1.2. SYMBOLIC COMPUTATION

23

Table 1-3. Maxima simplification functions.

combine()

distrib()

expand()

factor()

logcontract()

radcan()

ratsimp()

rootscontract()

trigexpand()

trigrat()

trigreduce()

trigsimp()

radcan(log(x*y^2/z)); − log (z) + 2 log (y) + log (x) trigexpand(sin(x + y)); cos (x) sin (y) + sin (x) cos (y) trigreduce(sin(x)*cos(y) + cos(x)*sin(y)); sin (y + x) Table 1-3 contains a list of just some of the many Maxima functions available to help you in the process of simplifying expressions. For the most part, the names of these functions tell you exactly what they do. Thus, ratsimp() simplifies ratios, logcontract() contracts logarithms, trigexpand() expands expressions containing trigonometric functions, and so on. One notable exception is the function radcan(), short for “radical cancellation,” which serves as a sort of all-purpose utility tool that can be useful for simplifying many different types of expressions. You can find more information about these and other functions by consulting the Maxima Manual [8], which you can access by selecting Help  Maxima Help from the drop-down menu or by pressing the question mark icon at the top of the Maxima window. This impressive list of functions might lead you to wonder why Maxima does not have a single function that can find the simplest

24

1. AN INTRODUCTION TO PROGRAMMING

form of any given expression. The answer is that any given expression might have more than one form that could be considered the simplest. For example, consider the polynomial: A : (x + x^2 + x^3 + x^4 + x^5 + x^6)^2;  6 2 x + x5 + x4 + x3 + x2 + x One way to simplify this polynomial is to decompose it into a product of prime factors: factor(A); 2  2 2 2 x +x+1 x2 (x + 1) x2 − x + 1 Another, would be to expand it out as a sum of monomials: expand(A); x12 + 2 x11 + 3 x10 + 4 x9 + 5 x8 + 6 x7 + 5 x6 + 4 x5 + 3 x4 + 2 x3 + x2 Which of these is the simplest depends on what we want to do with our polynomial. If our task is to find its x-intercepts, the factored version is better. However, if we wish to integrate it, the expanded version is better. This just goes to show that “shorter” is not necessarily “simpler”. Perhaps there are even some applications in which the original formulation of this polynomial, which incidentally is shorter than either of the other two, is the preferable one. Evidently, like beauty, simplicity is in the eye of the beholder. As a second example, consider the variety of different forms in which we might rewrite the following rational expression: B : (x^6 + 1)/(x+1)^6; x6 + 1 (x + 1)

6

For instance, we could factor the expression: factor(B);  2   x + 1 x4 − x 2 + 1 (x + 1)

6

Or we could distribute the terms in the numerator between two fractions:

1.2. SYMBOLIC COMPUTATION

25

distrib(B); x6 (x + 1)

6

+

1 (x + 1)

6

Or we could expand the denominator: ratsimp(B); x6

+

6 x5

15 x4

+

x6 + 1 + 20 x3 + 15 x2 + 6 x + 1

Or we might distribute the terms in the numerator over an expanded denominator: expand(B); x6 +

+

x6

6 x5 +

+

6 x5

15 x4 +

x6 + 20 x3 + 15 x2 + 6 x + 1

15 x4

1 + 20 x3 + 15 x2 + 6 x + 1

Again, note that whichever of these forms is preferable has little to do with which is longer or shorter, or the number of symbols used to write it, and instead depends on our particular mathematical needs. In addition to the simplification functions mentioned above, Maxima also provides us with a couple of useful shortcuts. For example, you can access any of the results already computed by entering its corresponding output label. Thus, you can recall the second result in this session by typing: %o2; 6.708203932499369 Furthermore, you can refer to the last result evaluated by the kernel by making use of the ditto operator (denoted by a percent sign %): %; 6.708203932499369 Assuming that you started a brand new Maxima session when you began reading this section and have entered all of the commands exactly as given above, you should see the same results.

26

1. AN INTRODUCTION TO PROGRAMMING

To see how the ditto operator can aid in symbolic computation, suppose that we wish to determine the radius and center of the circle given by the equation 9x2 + 42x + 9y 2 − 120y = 176. We can enter this equation into Maxima as follows: 9*x^2 + 42*x + 9*y^2 - 120*y = 176; 9 y 2 − 120 y + 9 x2 + 42 x = 176 Observe that using an equal sign does not have the same effect as using the assignment operator. In particular, the expression above does not change the value of any of the variables stored in memory. Instead, the equation above, by itself, represents an unevaluated statement that may or may not be true. In later sections (when we discuss conditional statements and iteration), we will evaluate equations like this and get a value of either true or false, but we do not need to worry about that now. Returning to the problem at hand, what we need to do is to complete two squares, thereby giving the equation a more recognizable form. We begin by dividing both sides of the equation by nine, in order to clear away the coefficients of x2 and y 2 . We do this using the ditto operator as follows: %/9; 9 y 2 − 120 y + 9 x2 + 42 x 176 = 9 9 expand(%); 176 40 y 14 x + x2 + = y2 − 3 3 9 Now, if we were completing the first square using paper and pencil, we would divide the coefficient of x by two, square it, and add it to both sides of the equation. Similarly, we would divide the coefficient of y by two, square it, and add it to both sides of the equation. This would produce the perfect squares 49 14 = u =x + x+ 3 9 2

2

 2 7 x+ 3

1.2. SYMBOLIC COMPUTATION

and v2 = y2 −

400 40 y+ = 3 9

 y−

20 3

27

2 .

In Maxima, we accomplish the same task by replacing x with the expression u - 7/3 with the substitution command subst() as follows: subst(x = u - 7/3, %);   7  2 14 u − 7 40 y 176 3 + + u− y2 − = 3 3 3 9 expand(%); 176 40 y 49 + u2 − = y2 − 3 9 9 Following the same recipe, we can complete the second square in our equation by replacing y with the expression v + 20/3: subst(y = v + 20/3, %);   20  2 40 v + 176 20 49 3 + w2 − = v+ − 3 3 9 9 expand(%); 176 449 = v 2 + u2 − 9 9 Finally, we obtain the desired formula for a circle by moving the constant terms to the right and substituting back for the dummy variables u=x+ % + 449/9; 625 v 2 + u2 = 9

7 3

and

v=y−

20 : 3

28

1. AN INTRODUCTION TO PROGRAMMING

subst([u = x + 7/3, v = y - 20/3], %);  2  2 7 625 20 + x+ = y− 3 3 9 It should  nowbe clear that the center of our circle is located at the point − 73 , 20 3 . The radius can be found by taking the square root of the right hand side of the equation, which we can extract with the rhs() function as follows:4 sqrt(rhs(%)); 25 3 Nearly all of the operators and functions listed in Tables 1-1, 12, and 1-3 can be applied to equations, as we did in the example above. The only exceptions to this rule are the function exp() and the operator ^ when used for exponentiation. However, we can use the operator ^ to raise both sides of an equation to a given power. Perhaps the most striking tool in Maxima’s symbolic computation arsenal is the command solve(), which can automatically find the solution to a large variety of equations. For example, we can find the unique solution to the linear equation 7x − 3 = 4x + 9 by entering the instruction solve(7*x - 3 = 4*x + 9); [x = 4] We can verify the validity of this solution by substituting it back into the original equation as follows: subst(%, 7*x - 3 = 4*x + 9); 25 = 25 We can use the same technique to solve a quadratic equation:

4 The analogous Maxima function to extract the left hand side of an equation is

lhs().

1.2. SYMBOLIC COMPUTATION

29

solve(x^2 - x = 1); √ √ 5−1 5+1 ,x = ] [x = − 2 2 This yields two solutions which Maxima reports within a list, separated by commas and surrounded by square brackets. (Technically speaking, the unique solution for the example above was also set in a list with only one item in it.) To access the contents of the list, we must use an indexing operator , which consists of a pair of matching square brackets [ · · · ] with a number in between them. In particular, we can assign the list of solutions above to the variable soln and then select the first of these by entering soln[1] and the second by entering soln[2]: soln : %; √ √ 5−1 5+1 ,x = ] [x = − 2 2 soln[1]; √ 5−1 x=− 2 soln[2]; √ 5+1 x= 2 As before, we can check the validity of these solutions by substituting them back into the original equation using subst(). However, as is often the case when dealing with complicated expressions, an additional application of radcan() is required to make the equality explicit: subst(soln[1], x^2 - x = 1); √ 2  √ 1− 5 1− 5 − =1 4 2 radcan(%); 1=1

30

1. AN INTRODUCTION TO PROGRAMMING

subst(soln[2], x^2 - x = 1); √ 2 √ 5+1 5+1 − =1 4 2 radcan(%); 1=1 We can also use solve() to find the solution to an equation involving more than one variable. In this case, however, Maxima must be told for which variable we are intending to solve. For instance, in order to solve the general quadratic equation a x2 + b x + c = 0, where x is the unknown and a, b, and c are coefficients, we enter the command: solve(a*x^2 + b*x + c = 0, x); √ √ b2 − 4 a c + b b2 − 4 a c − b ,x = ] [x = − 2a 2a Here, the second input tells Maxima to solve for x and not for one of the coefficients. The computational machinery employed by solve() is guaranteed to work for polynomial equations of degree four or less. In such cases, Maxima implements the quadratic formula above, as well as the analogous (though less well-known) cubic and quartic formulas.5 However, Maxima is not limited to such expressions. In addition to many quintic and higher-degree polynomials, solve() can also handle a variety of equations involving rational expressions, logarithms, exponentials, and trigonometric functions. For instance, the following equations are all easily solved by entering a one-line instruction: solve(x^5 - 5*x^3 + 4*x = 0); [x = −2, x = 2, x = −1, x = 1, x = 0]

5 Unfortunately, according to a famous theorem by Niels Henrik Abel (1802 – 1829), there is no general algebraic formula for solving polynomials of degree five or higher.

1.2. SYMBOLIC COMPUTATION

31

solve((x - 5)/(2*x - 3) = 4/x); [x = 12, x = 1] solve((3-log(x))*(3+log(x)) = 9 - log(x)); [x = 1, x = e] solve(2^(2*x-1/2) = 32*sqrt(2)); [x = 3] solve((sin(x))^2 = 1/4); solve: using arc-trig functions to get a solution. Some solutions will be lost. π π [x = − , x = ] 6 6 Take heed of that last warning: Although x = π/6 and x = −π/6 are two possible solutions, there are infinitely many others, including x = 5π/6, x = −5π/6, and all of their coterminal variants. The substantial mathematical knowledge displayed in the examples above leaves no doubt that Maxima can serve as a valuable computational assistant. However, we should not lose sight of the fact that, from time to time, Maxima will require our human guidance. After all, solve() follows a particular set of instructions (what, in computer science and mathematical circles, is referred to as an algorithm) that could never completely account for the intuition that a mathematician brings to bear when solving a problem. For instance, Maxima’s solution for the following equation hardly seems satisfactory until after we apply radcan(): solve(3^(x+1) = 27); [x =

log (27) − log (3) ] log (3)

radcan(%); [x = 2]

32

1. AN INTRODUCTION TO PROGRAMMING

The same can be said for this equation: solve(log(x-1)/log(4) = 1/2); log (4)

[x = e

2

+ 1]

radcan(%); [x = 3] On the other hand, in the following example, radcan() seems to have no effect on the final outcome. A little experimentation reveals (perhaps surprisingly) that expand() does the trick, providing a more compact answer: solve(5^(3-x) = 7); [x = −

log (7) − 3 log (5) ] log (5)

radcan(%); [x = −

log (7) − 3 log (5) ] log (5)

expand(%); [x = 3 −

log (7) ] log (5)

Finally, Maxima solves the following equation in terms of x1/3 , so we must cube the result in order to find the corresponding values of x: solve(x^(2/3) + 6 * x^(1/3) = -8); 1 1 [x 3 = −2, x 3 = −4] %^3; [x = −8, x = −64] Occasionally, Maxima’s first attempt at solving an equation fails altogether, and a little more human ingenuity is required from our part. In many cases, the fix involves applying radcan() before calling on solve(). For instance:

1.2. SYMBOLIC COMPUTATION

33

solve(x^log(x) = %e); [xlog(x) = e] radcan(%); 2 [elog(x) = e] solve(%); [x = e−1 , x = e] And similarly: solve(4^x + 8 = 9*2^x); [4x = 9 2x − 8] radcan(%); [22 x = 9 2x − 8] solve(%); [x = 0, x =

log (8) ] log (2)

radcan(%); [x = 0, x = 3] Sometimes, a little more firepower is needed. For example, the next equation requires both radcan() and log() before solve() can be invoked: solve(2^(-x^2) = 4^x/8); 2 [2x = 2 41−x ] radcan(%); 2 [2x = 23−2 x ] solve(%); 2 [2x = 23−2 x ]

34

1. AN INTRODUCTION TO PROGRAMMING

log(%); [log (2) x2 = log (2) (3 − 2 x)] solve(%); [x = −3, x = 1] At times, even more human ingenuity is called for. For instance, the following equation is unaffected by radcan() or by any of the other simplification functions like trigrat() or trigsimp() that are specifically designed to implement trigonometric identities. However it can be solved if we first divide by cos(x) and then transform the result using trigreduce(): solve(5*sin(x) - 3*cos(x) = 7*cos(x) + 2*sin(x)); [sin (x) =

10 cos (x) ] 3

%/cos(x); [

10 sin (x) = ] cos (x) 3

trigreduce(%); [tan (x) =

10 ] 3

solve(%); solve: using arc-trig functions to get a solution. Some solutions will be lost.   10 [x = atan ] 3 Similarly, in the next example, we first need to rearrange the equation so that the square root can be eliminated by squaring both sides. This yields a quadratic equation that solve() can readily tackle: solve(x - 15 = 2 * sqrt(x)); √ [x = 2 x + 15]

1.2. SYMBOLIC COMPUTATION

35

% - 15; √ [x − 15 = 2 x] %^2; 2 [(x − 15) = 4 x] solve(%); [x = 9, x = 25] Of course, it should be noted that squaring an equation, as above, has the potential of introducing false or spurious solutions, so we should do so only with extreme care. In this case, x = 25 is a valid solution, but x = 9 is spurious: subst(x = 25, x - 15 = 2 * sqrt(x)); 10 = 10 subst(x = 9, x - 15 = 2 * sqrt(x)); −6 = 6 Likewise, in order to solve the next equation we need to gather all of the various terms in the equation together under a single logarithm using logcontract(): solve(log(x) + log(x+9) = 2*log(6)); [log (x) = 2 log (6) − log (x + 9)] % - log(x); [0 = − log (x + 9) − log (x) + 2 log (6)] logcontract(%);   36 [0 = log ] x (x + 9) solve(%); [x = −12, x = 3] Unfortunately, x = −12 is also a spurious solution since, when substituted back into the original equation, it gives rise to the logarithm of two negative numbers:

36

1. AN INTRODUCTION TO PROGRAMMING

subst(x = -12, log(x) + log(x+9) = 2*log(6)); log (−3) + log (−12) = 2 log (6) float(%); 6.283185307179586 i + 3.58351893845611 = 3.58351893845611 Observe that each spurious solution above was actually a valid solution for some intermediate equation (the result of either squaring or contracting logarithms) that was only partially equivalent to the original equation. Evidently, the fault in those cases lies in the human who gave the instruction to transform the equation at that intermediate stage. On the other hand, the prospect of introducing spurious solutions was offset by the valid solutions that would have otherwise remained hidden. Our final example shows that Maxima is not above making a similar type of algebraic gamble: solve((x^3 - 1)/x = 2*x - 1/x); [x = 0, x = 2] Here, x = 2 is valid but x = 0 is spurious as it would lead to division by zero: subst(x = 0, (x^3 - 1)/x = 2*x - 1/x); expt: undefined: 0 to a negative exponent. – an error. To debug this try: debugmode(true); Evidently, in the process of solving this equation, Maxima must have introduced the spurious solution by multiplying times x. As we have seen in the examples above, using Maxima effectively is sometimes more of an art than a science. More than anything, these examples highlight the need for a careful analysis of our computational results—especially those produced by a sophisticated algorithm like that behind the solve() command. In such cases, it can take an attentive eye to detect potential problems, so we should be mindful of the critical balance between Maxima’s computational machinery and our own human insight.

Exercises for Section 1.2 1. Use the simplification functions listed in Table 1-3 to find as many different forms as possible for each of the following expressions. Which function gives the most compact form for each expression? How about the longest form? (a)

x+1 x3 + 4 x2 + 5 x + 2

1 1 1 − − 4(x − 1) 2(x2 + 1) 4(x + 1)  n 2  2 x − 1 · xn + 1 (c) x2n − 1

(b)

(d)

3 cos2 x sin x − sin3 x sin x − 6 cos2 x sin2 x + cos4 x

(e)

2 log(cos x) − log(1 − sin x) log(2(sin x + 1) − cos2 x)

4

(f) 2x − 4x cos2 x + 2 cos x sin x 2. Use Maxima’s symbolic computation tools to find the solutions to the following equations. In each case, indicate which solutions are valid and which are spurious. (a) x2/3 + 6 x1/3 = 7 (b)



x+2=x  √ 15 −2= x (c) x

(d) 49x = 71−x (e) 31−2x = 27x (f) ex

2

+6

2

= e5x

3. When the global variable solveradcan is set to true, Maxima automatically passes both the input and output of a function call to solve() through radcan(). This gives solve() more power but it also makes it slower. How does this change affect Maxima’s

38

1. AN INTRODUCTION TO PROGRAMMING

behavior when solving the following equations? Compare your results with the corresponding examples described in this section. √ (a) 22x−1/2 = 32 2 (b) 3x+1 = 27 (c) log4 (x − 1) =

1 2

(d) 53−x = 7 (e) xlog x = e (f) 4x + 8 = 9 · 2x 4. The package to_poly_solve includes a function called %solve() that implements an alternate algorithm for solving equations. The syntax is similar to that of solve(), except that the name of the variable to be solved for is required. To load the package and solve a few equations, we would enter: load(to_poly_solve) $ %solve((x - 5)/(2*x - 3) = 4/x, x); %union ([x = 1], [x = 12]) %solve((3-log(x))*(3+log(x)) = 9 - log(x), x); %union ([x = 1], [x = e]) %solve(2^(2*x-1/2) = 32*sqrt(2), x);   i π %z21 + 3 log (2) %union [x = ] log (2) %solve((sin(x))^2 = 1/4, x);  π −2 π %z31 − − 2 π %z29 ], [x = − %union [x = − 3 2 2

π 3

 ]

Note that %solve() packages its responses inside %union() structures instead of lists. In addition, when an infinite number of solutions are found, these are given in terms of generic variables like %z21, %z29, and %z31 above. These variables can be replaced by any integer. For instance, replacing each one with zero in the

EXERCISES

39

solutions above produces: %z21 = 0



x = 3,

%z29 = 0



x = − π6 ,

%z31 = 0



x=

π . 6

Investigate Maxima’s ability to solve the following equations using %solve(). Compare your results with the corresponding examples described in this section. 2

(a) 2−x = 4x /8 (b) 5 sin x − 3 cos x = 7 cos x + 2 sin x √ (c) x − 15 = 2 x (d) log x + log(x + 9) = 2 log 6 (e) xlog x = e (f) x2/3 + 6x1/3 = −8 5. We can interpret the coefficients of the polynomial f (x) = x + x2 + x3 + x4 + x5 + x6 as the relative frequencies observed when rolling a standard sixsided die. In this case, the equal coefficients reflect a uniform probability distribution in which each of the six results (encoded as the various powers of x) is equally likely. Squaring this polynomial has the same effect as rolling two identical dice simultaneously:  2 A(x) = f (x) = x 2 + 2 x3 + 3 x4 + 4 x 5 + 5 x6 + 6 x7 + 5 x8 + 4 x9 + 3 x10 + 2 x11 + x12 . Indeed, just as there are six different ways to roll a seven but only one way to roll either a two or a twelve, the coefficient of x7 is 6 while those of x2 and x12 are 1. (a) Find all the possible ways to factor A(x) as a product p(x) · q(x) of two polynomials with integer coefficients such that

40

1. AN INTRODUCTION TO PROGRAMMING

p(0) = q(0) = 0 and p(1) = q(1) = 6. Explain how you know that you have accounted for all possibilities. Hint: Start with the complete factorization for A(x) found in page 24 and decide which of those factors must belong to p(x) and which must belong to q(x). (b) Each factorization from part (a) can be interpreted as an alternative labeling on a pair of six-sided dice. Show that, when rolled together, these “alternative dice” produce the same relative frequencies as a standard pair of dice. For instance, you might list all of the different ways to roll these alternative dice and get a two, a three, a four, and so on, up to twelve.

1.3. User-Defined Functions Before we can properly apply the term “programming,” we must go beyond the basic computational elements introduced in the last two sections and instead turn our attention to creating new functions of our design. In particular, we shall now discuss Maxima’s function definition operator , which is denoted by a colon-equals (:=). Defining a function requires a deeper sort of knowledge than computing a single, specific result. For example, √ it is easy enough to determine that 12 squared equals 144 or that 5 squared equals 5. However, defining a function for squaring requires that you know how to square any number. Indeed, when you define a function, you are playing the role of the “programmer” who implements a function in terms of some generic set of inputs, so that someone else—the “user”— can call that function later on with a specific set of input values. Suppose, then, that you want to explain to one of your human friends the idea of squaring. You might say, “To square something, you multiply it times itself.” The same idea can be expressed to Maxima with the following function definition: square(x) := x*x $ You can understand this definition as follows: You want to square “something.” In this case the “something” is represented by the input x. In order to square x, you multiply x times itself. Thus, the idea of squaring x is captured by the expression x*x. This rule is then stored as our new user-defined function called square(). Having defined the function square(), you can now use it in exactly the same way as if it were a primitive function like exp() or sqrt(). For instance, you can use square() as part of a simple arithmetic expression: square(12); 144 combine it with arithmetic operators: square(3 + 4); 49 nest other functions inside it:

42

1. AN INTRODUCTION TO PROGRAMMING

square(sqrt(5)); 5 nest it inside other functions: sqrt(square(7)); 7 or any combination of the above. You can even use square() as a building block in defining other functions. For example, you can define a function that, given any two numbers, produces the sum of their squares: sum_of_squares(x, y) := square(x) + square(y) $ sum_of_squares(5, 12); 169 As with variables, the kernel keeps a record of all user-defined functions defined during a Maxima session; this record is stored in the list functions, which you may examine at any time as follows: functions; [square, sum of squares] You can see the function definition associated with any one of these names by calling on the function fundef(): fundef(square); square(x) := x x You can also remove function names from the list using the kill() function. Of course, this also has the effect of deleting our function definition altogether from the kernel’s memory. Although it is not recommended, it is possible to assign both a value and a function definition to the same name. In this case, the name would appear in both the values and the functions lists, and the kill() function would simultaneously delete the name from both lists. To remove a name from only one list or the other, you can use the functions remvalue() or remfunction(), respectively. All things considered, both square() and sum of squares() are relatively simple functions which prompt Maxima to evaluate a single expression. On the other hand, we can construct functions with truly

1.3. USER-DEFINED FUNCTIONS

43

complex behaviors by bundling together several expressions into a code block . The block() command has the following basic syntax: block([local variables], body, return(final value)) In particular, each block consists of three parts: x A list of local variables (surrounded by square brackets and separated by commas) that Maxima will use as it evaluates the contents of the block. y The body of the block, comprising a list of instructions (separated by commas) to be executed in sequence by Maxima. z The final value to be returned by Maxima when it reaches the end of the block. For instance, we might redefine our function sum of squares() using a code block with two local variables (each assigned the square of one of the inputs) as follows: sum_of_squares(x, y) := block([a, b], /* compute the square of each input, then add the two together and return the result */ a : square(x), b : square(y), return(a + b)) $ Defining a single function typically requires entering more than one line of code in a single cell. To do this, you simply press Enter (without holding down the Shift key) when you reach the end of each line in the definition. This will move the cursor to a new line in the cell without invoking the kernel. While you are at it, you can add extra spaces to indent the internal lines of a definition. You can even introduce completely blank lines to separate different parts of a function, all for the sake of clarity. Maxima will completely ignore all of this white space and it will have no bearing on the final result. You will also observe that we included a couple of comments surrounded by the symbols /* and */ in the middle of our function definition. These comments are useful for those humans who might be trying to understand our code, but they are also ignored by Maxima. Finally,

44

1. AN INTRODUCTION TO PROGRAMMING

Begin with x, y

Set a : x2

Set b : y2

Return a+b

Fig. 1-3. Flowchart for sum of squares(). you should note that the output of a function definition is usually an unpleasant repetition of our code that can quickly clutter our Maxima session. For this reason, we will finish our definitions with a dollar sign, thereby skipping the display step of the usual read-evaluatedisplay cycle. Once you are finished typing the entire definition, just press Shift-Enter as usual to send your instructions to the kernel. As soon as we call on this new version of sum of squares(), Maxima will set apart some portion of memory to use as she evaluates the code block in the function definition. This section of memory, called the local environment, will contain the values assigned to the inputs x and y, as well as the local variables a and b. For example, if we enter the expression sum_of_squares(5, 12); 169 Maxima will set up a local environment with x set to 5 and y set to 12. It will then proceed to evaluate the code block line by line, first by computing x2 = 25 and storing its value in a, then by computing y2 = 144 and storing its value in b, and finally by adding a and b together and returning the result. The flowchart in Fig. 1-3 gives a graphical representation of this process. Because a function’s inputs and local variables are stored in the local environment, the names that you use to denote them are completely internal to the function. In particular, you do not need to worry about using the same variable name in another function definition. Nor should you worry about whether a variable name is already assigned to a “global” value outside the local environment. For the purposes of evaluating a given function, Maxima will completely ignore these external values and only use the values stored inside the local environment.

1.3. USER-DEFINED FUNCTIONS

45

On the other hand, when Maxima encounters a variable name that is not local, she will look for the value that was assigned to that name most recently, either as an explicit global variable or as a local variable in a function calling the present function. This strategy is known as dynamic scoping . For instance, consider the following fanciful definitions: groucho(x) := block([b], b : a, a : x, print("Groucho changed a from", b, "to", a), return(a)) $ harpo(x) := block([a, b], a : x, groucho(x*2), b : a, a : a+2, print("Harpo changed a from", b, "to", a), return(a)) $ zeppo(x) := block([b], groucho(x+3), harpo(x*3), b : a, a : x, print("Zeppo changed a from", b, "to", a), return(a)) $ Observe that all three of the functions defined above have a local variable named b. Thus, when each of them tries to change the value of b, the change is restricted to the local environment. However, only one of the functions has a local variable named a, even though all three try to change its value, either directly or indirectly. Before any changes can be made, Maxima must first decide which location in memory should be affected by these changes, and this is where dynamic scoping comes in. For example, when groucho() is called

46

1. AN INTRODUCTION TO PROGRAMMING

Global Environment a: 3 5

Local Environment groucho(5) x: 5 b: 3 Return value: 5

Fig. 1-4. The dashed arrow indicates that the function groucho(), when called directly, changes the global value of a from 3 to 5.

directly, Maxima first looks for a variable called a in the local environment, and finding none, she moves on to the global environment, that is to say, the portion of the kernel’s memory to which all functions have access. In this case, the change in value occurs globally, as illustrated schematically in Fig. 1-4: a : 3; 3 groucho(5); Groucho changed a from 3 to 5 5 a; 5 In contrast, when groucho() is called from within the body of another function like harpo(), Maxima begins its search for a local variable called a in groucho()’s local environment, and then moves on to harpo()’s local environment. Since there is a variable named a

1.3. USER-DEFINED FUNCTIONS

47

Global Environment a: 5 Local Environment harpo(3) x: 3 a: 3 6 8 b: 6 Local Environment groucho(6) x: 6 b: 3 Return value: 6 Return value: 8

Fig. 1-5. The function groucho(), when called from within harpo(), changes the value of a that is local to harpo(). The global value of a remains untouched.

there, all of the changes in value performed by both groucho() and harpo() will occur locally in that local environment and not globally, as shown in Fig. 1-5: harpo(3); Groucho changed a from 3 to 6 Harpo changed a from 6 to 8 8 a; 5

48

1. AN INTRODUCTION TO PROGRAMMING

Global Environment a: 5 7 4

Local Environment zeppo(4) x: 4 b: 7 Local Environment groucho(7) x: 7 b: 5 Return value: 7

Local Environment harpo(12) x: 12 a: 12 24 26 b: 24 Local Environment groucho(24) x: 24 b: 12 Return value: 24 Return value: 26

Return value: 4

Fig.1-6. The function groucho() changes the global value of a when called from within zeppo(), but only a local value of a when called from within harpo().

Finally, note that the function zeppo() first calls groucho() and then harpo(), who in turn calls groucho() a second time. On the first call to groucho(), Maxima looks for a variable called a in groucho()’s local environment, then in zeppo()’s local environment (since zeppo() called groucho()), and then in the global environment. Thus, the change in the value for that first function call is global. On the other hand, when groucho() is called a second time, Maxima first looks for a in groucho()’s local environment, and then in harpo()’s local environment (since harpo() called groucho()). Since she succeeds in finding a there, the change that occurs in this second function call to groucho() is only local to harpo(). Lastly, before returning its final value, zeppo() changes the value of a once more; in this case, the change is again global, as shown in Fig. 1-6.

1.3. USER-DEFINED FUNCTIONS

49

zeppo(4); Groucho changed a from 5 to 7 Groucho changed a from 12 to 24 Harpo changed a from 24 to 26 Zeppo changed a from 7 to 4 4 a; 4 Even with the addition of code blocks, the type of functions that we can define at this point is very limited. In particular, functions like sum of squares() simply go through the instructions in their definitions in a straightforward linear fashion. Quite often, however, we will want a function to perform some sort of test, and depending on the outcome of the test to execute a different set of operations, allowing the computational process to evolve in different ways for different inputs. We might even want to repeat the same set of instructions some number of times before returning a final value. For instance, consider a function that looks at two numbers and returns the larger one of the two. We might describe this function mathematically by the rule ⎧ ⎨x if x > y, bigger(x, y) = ⎩y otherwise. This type of construct, which is called a conditional statement, is illustrated by a branching flowchart like the one in Fig. 1-7. We can implement this function in Maxima as follows: bigger(x, y) := block( /* conditional tests which input is bigger */ if x > y then return(x) else return(y)) $

50

1. AN INTRODUCTION TO PROGRAMMING

yes

Begin with x, y

Is x > y?

Return x

no Return y Fig. 1-7. Flowchart for bigger().

There are two things to notice about this function definition. First of all, the usual list of local variables is missing. This is because the only two pieces of data needed to reach a final value are the inputs themselves, so any additional variables would be superfluous. The second thing to note is that a return() command can appear multiple times in a block. In the example above, one appears at the end of each “branch” of the conditional. The first time that Maxima encounters one of these commands, she stops evaluating the remainder of the function and returns the indicated value. The choice of which branch of the conditional statement is to be followed depends on the expression x > y.

Table 1-4. Maxima’s six comparison operators. operator

comparison

=

equal

#

not equal

>

greater than


=

greater than or equal

0, or ◦ the algorithm has performed a maximum number of iterations N . You may base your implementation on the values δ = 10−10 , ε = 10−10 , and N = 50. (c) Insert appropriate print() commands to your bisect() function to report the length of the final interval, the absolute value of f at the last midpoint, and the total number of iterations performed. Then find an example of a function and an interval for which the bisection method stops under each of the conditions described in part (b). 3.

(a) Implement a Babylonian-like algorithm for computing the cube root of a using the following rule to improve your guesses:   1 a xk+1 = xk + 2 . 2 xk (b) Implement a Babylonian-like algorithm for computing the cube root of a using the following rule to improve your guesses:   1 a xk+1 = 2xk + 2 . 3 xk (c) Compare the algorithms from parts (a) and (b). Which of these methods converges faster to the cube root of a? Be sure to test your answer using more than one value of a.

4. Consider the following algorithm for finding numerical solutions to the equation f (x) = 0 under the same conditions the bisection method: Instead of dividing the interval between a and b at the midpoint, divide it proportionally according to the absolute value of f at each endpoint. In other words, at each iteration, set c=

a · f (b) − b · f (a) . f (b) − f (a)

This is known as the regula falsi method (Latin for “false rule”) and is often more effective than the bisection method since, all

146

2. COMPUTATION IN MATHEMATICS

other factors being equal, a solution is more likely to occur closer to the endpoint having the smaller absolute value of f . (a) Implement the regula falsi method using the same stopping condition as that used by the bisection method. (b) Suppose that you are trying to find the square root of two. Assuming that you start with the same initial conditions, which algorithm is faster, the bisection method or the regula falsi method? How many steps does each algorithm take? 5. Newton’s method is an algorithm that lets us find the roots of a differentiable function f . We start with some initial guess x1 and improve this guess by following the tangent line from the point (x1 , f (x1 )) down to the x-axis, to the point (x2 , 0). Then we take x2 as our next guess and repeat the process. In the kth step, this amounts to setting xk+1 = xk −

f (xk ) . f  (xk )

This algorithm was first described by Isaac Newton (1643 – 1727) in a pair of papers written in 1669 and 1671. However, as with much of Newton’s work, these were not published until significantly later, in 1711 and 1736 respectively. (a) Design a function Newton() that computes n iterations of Newton’s method. In your definition, you should implement the function f as a formula in terms of the free variable x, and use the command diff(f, x) to determine its derivative. As in the case of the bisection method, you can use the command subst() to evaluate these two formulas at the appropriate points. (b) Suppose that you are trying to compute the square root of two. Assuming that you start with the same initial guess, how do the intermediate approximations produced by this method compare with those from the Babylonian algorithm? Explain why this is the case.

2.3. Detecting Prime Numbers If the Babylonian algorithm is among one of the oldest on record, then it is closely followed by the method of “sifting out” prime numbers known as Eratosthenes’ sieve. It was originally discovered by Eratosthenes of Cyrene (c.276 BC – 195 BC), whom you may have already met in a couple of exercises in Chapter 1. Before discussing his algorithm, let us start with a few preliminaries. As you may recall, a whole number is said to be prime if it is divisible by exactly two distinct factors—one and itself. If it is divisible by more than two factors, then is said to be composite. Therefore 2 and 7 are prime, while 6 and 15 are composite. The number 1 is neither prime nor composite since it is divisible by one and only one factor. Maxima provides us with the predicate primep() that can help us determine whether or not a number is prime. For example: primep(2); true primep(7); true primep(6); false primep(15); false primep(1); false As we shall soon see, the inner workings of this predicate depends on some rather sophisticated mathematics. In one of the most beautiful proofs of all time, Euclid of Alexandria (c.300 BC) showed that there are infinitely many prime numbers. This means that we could never write down a complete list of all prime numbers. However, Eratosthenes’ sieve helps us to make a partial list containing all of the prime numbers up to some given upper bound n. The basic strategy is to start with a list of all integers from 2 to n and to “sift out” those numbers on the list that we know cannot be prime. We do this as follows:

148

2. COMPUTATION IN MATHEMATICS

Eratosthenes’ Sieve: x Start with the list {2, 3, 4, . . . , n}. y Let p = 2 be the first number on the list; mark it as a prime. z Remove all multiples of p from the list. { Let p be the next number remaining on the list; since this number was not removed from list earlier, it must not have any factors other than one and itself and is therefore prime. | Repeat steps z and { until the list is empty. The function definition below gives a Maxima implementation of this algorithm. To start, we create our “sieve” using the makelist() command, noting that, since this list starts with 2, each integer i is placed in the (i − 1)th position of the sieve. We will remove numbers from this sieve by replacing their corresponding values with a zero. This involves two nested for loops: First we have an “outer loop” with counter p that considers each of the numbers in the sieve in turn; those that are prime are selected by the conditional if-then command, which detects that they were not removed from the sieve in an earlier iteration. As new primes are found, they are accumulated (using the concatenation function endcons()) into the list primes, which starts out empty. Then, an “inner loop” with counter i removes the various multiples of p from the sieve. The print() command tucked at the end of the outer loop allows us to follow our function’s progress as it sifts out primes from composites: Eratosthenes(n) := block([sieve, primes, p, i], /* start with sieve = [2, 3, 4, 5, ..., n] */ sieve : makelist(i, i, 2, n), primes : [], /* outer loop finds next number still in sieve and adds it to list of primes */ for p : 2 thru n do

2.3. DETECTING PRIME NUMBERS

149

if sieve[p-1] # 0 then (primes : endcons(p, primes), /* inner loop removes multiples of p from sieve */ for i : p thru n step p do sieve[i-1] : 0, print(p, ": ", sieve)), return(primes)) $ Eratosthenes(25); 2 : [0, 3, 0, 5, 0, 7, 0, 9, 0, 11, 0, 13, 0, 15, 0, 17, 0, 19, 0, 21, 0, 23, 0, 25] 3 : [0, 0, 0, 5, 0, 7, 0, 0, 0, 11, 0, 13, 0, 0, 0, 17, 0, 19, 0, 0, 0, 23, 0, 25] 5 : [0, 0, 0, 0, 0, 7, 0, 0, 0, 11, 0, 13, 0, 0, 0, 17, 0, 19, 0, 0, 0, 23, 0, 0] 7 : [0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 13, 0, 0, 0, 17, 0, 19, 0, 0, 0, 23, 0, 0] 11 : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 0, 0, 0, 17, 0, 19, 0, 0, 0, 23, 0, 0] 13 : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 19, 0, 0, 0, 23, 0, 0] 17 : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 23, 0, 0] 19 : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 0, 0] 23 : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [2, 3, 5, 7, 11, 13, 17, 19, 23] How long does it take Eratosthenes() to find all primes up to a given n? To give an adequate order of growth estimate, we need to observe that roughly half of the integers in the sieve are multiples of 2, that roughly one third of them are multiples of 3, and that, in general, roughly p1 of them are multiples of p. This means that in the various iterations of the inner loop, our procedure must remove approximately N≈

n n n n + + + + ··· + 1 2 3 5 7

numbers from the sieve, some of which are removed more than once. This finite sum is approximately equal to n times a partial sum of the

150

2. COMPUTATION IN MATHEMATICS

so-called “prime harmonic series”  1 1 1 1 1 = + + + + ··· , p 2 3 5 7 p prime   whose partial sums grow like the function log log(n) . This allows us to say that   N ≈ n log log(n) . Assuming that the bulk of the work done by our function comes from removing these numbers from the sieve,2 we can conclude that Eratosthenes() is slower than a linear-time algorithm, but still much quicker than a quadratic-time algorithm, or indeed any algorithm making nq steps with q > 1. Eratosthenes’ sieve is an excellent method to find prime numbers en masse, but it is not very efficient for determining whether or not a given number n is prime. Just in terms of memory usage, this would require a sieve of size n, which could be considerable when n is a large number. Nevertheless, it is easy to adapt this algorithm into one that does not require all that much space to determine whether n is prime. All we would need to do is test whether n would have been removed from the list at some point in the sifting process. In particular, we only need to check and see whether n is divisible by some number less than or equal to its square root, since any larger factor would necessarily imply the existence of a smaller one. This na¨ıve primality test can then be described as follows: Eratosthenes Primality Test: x Start with √ i = 2 (assuming that this is less than or equal to n). y Check whether i divides evenly into n; if it does then n is not prime. √ z Increase i and repeat step y until i > n. { If no factor of n has been found, then n is prime. Below we give an implementation of this simple primality test. The heart of our function is a while loop that searches for a factor of the input n, starting with i = 2. The loop calls on the primitive function 2 As we shall see in the next section, this assumption depends on the underlying

implementation of lists and although it is valid for many programming languages, it is not entirely sound for Maxima.

2.3. DETECTING PRIME NUMBERS

151

mod() to compute the remainder when n is divided by i, or what in number theory is referred to as “n modulo i.” If the result is zero, then i is a factor and n is not prime; otherwise, the loop continues with the next value of i. In order to bring the loop to an end when a factor is found, we employ the local variable done to serve as a flag indicating whether or not a factor has been found. Initially, the flag has the value of false. If a factor is found, then its value is changed to true causing the loop to stop. If, on the other hand, the loop runs its course and the flag √ is not changed, then n must be prime since all of the integers up to n have been scanned to no avail. In either case, the question of whether n is prime is answered by the final value of not(done). primetest(n) := block([i, done], i : 2, done : false, /* test whether n is divisible by some number i 0 is even, and n > 0 is odd */ if n = 0 then return(1) elseif evenp(n) then return(mod(square(modexp(b, n/2, m)), m)) else return(mod(b * square(modexp(b, (n-1)/2, m)), m))) $ Finally, we can define a function that performs one instance of the Fermat test by letting pick() choose a random value of a between 2 and n − 1, calling on modexp() to compute an−1 modulo n, and then returning a value of true or false depending on whether this remainder is equal to 1: Fermat(n) := block([a], /* pick a random value of a between 2 and n - 1 */ a : pick(n), /* verify Fermat’s Little Theorem */ if modexp(a, n-1, n) = 1 then

2.3. DETECTING PRIME NUMBERS

155

return(true) else return(false)) $ Observe that the Fermat primality test is unlike any of the algorithms we have studied thus far in two important ways. First, each of our earlier algorithms always returned the same answers when given the same input; in other words, they were deterministic. In contrast, the Fermat test is probabilistic, meaning that it can give different results for the same input, depending on the random value of a that is chosen. Furthermore, our earlier algorithms produced answers that were guaranteed to be correct; for the Fermat test, any positive results are only “probably correct.” For instance, any number that fails the test is definitely not prime: Fermat(8535937); false However, when a number passes the test, all we can say is that it probably is prime: Fermat(8675309); true In other words, the Fermat test gives us strong, but not conclusive, evidence that a given number is prime. In such a case, we could run the test a second time, picking another random value for a and testing it with the same method. If a number passes the test a second time, then we can be even more confident that it is prime. By trying more values of a, we can further increase our confidence in the result. For example, we know that 561 is not prime since it is divisible by 3, and yet it passes the Fermat test about 57% of the time: Fermat(561); true Fermat(561); false Numbers, like 561, that fool the Fermat test a large portion of the

156

2. COMPUTATION IN MATHEMATICS

time are known as Carmichael numbers, after the American mathematician Robert Carmichael (1879 – 1967). Because of the existence of these numbers, finding that a number passes the test several times can increase our confidence that it is prime, but the chances that such a prognosis is incorrect are never completely eliminated. For instance, the probability that 561 will pass the Fermat test ten consecutive times is less than 0.004, 0.57^10; 0.003620333314568909 making it highly unlikely but still possible. Since Carmichael numbers are extremely rare, the Fermat test is considered quite reliable in practice. Furthermore, the test can be modified so that it cannot be fooled indefinitely. When testing large values of n, Maxima’s primep() predicate implements one of these “foolproof” variations known as the Miller-Rabin test (see Exercise 5). As in the Fermat test, this probabilistic algorithm picks a number at random and checks that it satisfies a condition that is guaranteed to hold whenever n is prime. If it fails the condition, then we know for certain that n is not prime; if the condition is satisfied, then the probability that n is composite is less than 0.25. This means that by running the test multiple times, one can make the probability of misdiagnosing a composite number as small as we like. In particular, Maxima repeats the test a total of 25 times, making the probability of error smaller that 10−15 : 0.25^25; 8.881784197001252 10−16 Until fairly recently, the best hopes for detecting prime numbers were found in probabilistic algorithms like the Fermat and MillerRabin tests. However, a revolutionary breakthrough was made in 2002, when Mahindra Agrawal, Neeraj Kayal, and Nitin Saxena, three Indian computer scientists discovered a deterministic primality test 7.5 that works in roughly (log(n)) steps.3 This is still not fast enough to compete with the speed records of the earlier probabilistic methods, which run in logarithmic time, but it seems to indicate that the tide might be finally shifting back toward deterministic primality tests.

3 An excellent exposition of this discovery is provided by Granville [5].

Exercises for Section 2.3 1. Consider the following sieve-type algorithm: Mystery Sieve Algorithm: x Create a list of consecutive integers from 1 to n. y Initially, let k = 2. z Consider each of the multiples of k less than or equal to n and either place or remove a mark on it as follows: ◦ If you encounter an unmarked multiple, mark it with a zero. ◦ If you encounter a multiple that has already been marked, remove the mark by restoring to its original value. { Increase k by one. | Repeat steps z and { as long as k ≤ n. Implement this algorithm as a Maxima function and run it for n = 100. Which numbers are sifted out by this function? Explain how this is accomplished. 2. In 1742, the German mathematician Christian Goldbach (1690 – 1764) conjectured that every even number greater than 2 can be written as the sum of two primes. This stands as one of the great open problems in number theory. (a) Design a Maxima function Goldbach() that takes an even integer n and finds a way of writing it as a sum of two primes. Hint: Consider every way to write n as a sum of the form x + (n − x), and use Maxima’s built-in predicate primep() to test whether x and n − x are both prime. (b) Show that every even number between 2 and 100 can be written as the sum of two primes. Note that you probably do not want to do all of the checking by hand! 3. Very little is known about Carmichael numbers, except that they are extremely rare. There are only 255 Carmichael numbers below 100 000 000, the smallest of which are 561, 1105, 1729, 2465, 2821, and 6601. (a) Verify for yourself that primetest() detects each of these numbers as being composite, but that Fermat() sometimes does not.

158

2. COMPUTATION IN MATHEMATICS

(b) For each of the Carmichael numbers listed above, find the probability that Fermat() is fooled. In other words, for each Carmichael number n determine the proportion of a’s between 2 and n − 1 for which an−1 equals 1 modulo n. (c) What is the probability that each of the Carmichael numbers above will pass the Fermat test ten consecutive times? 4. The largest prime number known to date, discovered by the Great Internet Mersenne Prime Search project (www.mersenne.org) in January 2016, is: p = 277 232 917 − 1. It turns out that this number is much too large for Maxima to handle either as an exact integer or as a floating-point number, but it can be represented as a big floating-point number, in which case, Maxima will display it in scientific notation, with the order of magnitude appearing after the letter “b.” (a) How many digits would be required to write down this number in its entirety? Express your answer as an exact integer. (b) How many remainders must primetest() perform before it can certify that p really is prime? Express your answer as a big floating-point number. (c) How many multiplications must Fermat() perform in order to compute ap−1 modulo p for a single value of a? Express your answer as an exact integer. (d) Assuming that Maxima takes one millisecond to compute one remainder or to perform one multiplication, how long would it take each of these functions to determine that p is prime? Express your answer in whatever units of time seem most reasonable. 5. The Miller-Rabin test used by primep() was developed by Gary Miller and Michael Rabin, appearing in a pair of publications in 1976 and 1980. It depends on the Fermat test and on the existence of nontrivial square roots of one modulo n, that is, numbers strictly between 1 and n − 1 whose square is equal to 1 modulo n. If such a number exists, then n is certainly not prime. (a) Define a function square1() that can be used to square a number modulo n while checking whether it is a nontrivial square root of one. In particular, your function should determine the value of r = mod(x2 , n) and return

EXERCISES

 square1(x, n) =

0 r

159

if r = 1 and 1 < x < n − 1, otherwise.

Check that your function works as expected by verifying that 67 and 307 are two nontrivial square roots of one modulo 561. (b) Define a function modexp1() that computes the value of bn modulo m, calling square1() to perform each squaring step. Check that your function returns a value of zero whenever it discovers a nontrivial square root of one in the process of completing the exponentiation. (c) According to the Miller-Rabin test, for any odd number n that is not prime, the probability of choosing a value of a with an−1 ≡ 1 mod n that does not reveal a nontrivial square root of one in the process of computing this exponential is less than 0.25. Verify this claim by determining, for each Carmichael number n listed in Exercise 3, the proportion of a’s between 2 and n−1 that fool the Miller-Rabin test.

2.4. Lists and Other Data Structures We have already seen several applications of Maxima’s list structure. In particular, various of the plots in Chapter 1 as well as our implementation of Eratosthenes’s sieve in Section 2.3 depended on the use of lists. Those earlier examples taught us how to: x create lists using the command makelist(), y concatenate items to the end of a list using the function endcons(), and z access and modify the contents of a list using the indexing operator [ · · · ]. Table 2-3 contains several other primitive Maxima functions for the manipulation and transformation of lists. In this section, we will take a closer look at some of these functions and at Maxima’s implementation of lists in general. Of course, Maxima is not the only programming language that provides a data structure that behaves like a list. In fact, some of the earliest “high-level” languages like Fortran and Cobol implemented a list-like structure called an array , which consisted of several contiguous units of memory containing the information in the list. For example, an array containing the first five prime numbers might be stored in five consecutive units of memory, each one large enough to store an integer, as indicated by the schematic diagram in Fig. 2-5. The benefit of placing all of an array’s elements in close proximity to one another is that it makes it extremely easy and fast to access and modify this data. For instance, in our sample array in Fig. 2-5, Table 2-3. Maxima list manipulation functions.

append()

assoc()

cons()

copylist()

delete()

emptyp()

first()

flatten()

join()

last()

length()

listp()

member()

rest()

reverse()

sort()

162

2. COMPUTATION IN MATHEMATICS

...

2009

2010

2011

2012

2013

2014

2015

20

2

3

5

7

11

17

...

Fig. 2-5. A schematic diagram of an array representing the list [2, 3, 5, 7, 11]. Each block consists of a unit of memory large enough to store an integer, while each number above it gives that unit’s address.

the first element is stored at a location in memory with an address of 2010, the second element is stored at address 2011, the third at address 2012, and so on. In other words, each element of the array can be located by a simple combination of its index and the first element’s memory address, and can thus be completed in a constant amount of time. Unfortunately, arrays suffer from two key disadvantages. First of all, the elements in an array must all be of the same type (that is to say, they must all be integers, or all be floating-point numbers) since that have to fit in memory units of the same size. Furthermore, the size of an array is determined at the outset, when it is first defined, and cannot be modified later. For example, note that there is no room to expand the array in Fig. 2-5 since the memory cells directly before and after the array already contain other data. This means that the only way to add data to an existing array is to copy the entire array’s data into a larger array elsewhere in memory. In place of arrays, Maxima provides a more versatile data structure known as a linked list. The glue that holds a linked list together is a simple object called a node, which we will denote schematically by two side-by-side square boxes. Each box in the node contains a pointer , that is to say, an address pointing to some other location in the computer’s memory. These pointers, which we will denote by arrows, can then be used to point to the various data elements in the list as well as to link nodes together to form more complicated data structures. For example, consider the following list: waders : [egret, greatblueheron, spoonbill] $

2.4. LISTS AND OTHER DATA STRUCTURES

163

waders

egret

greatblueheron

spoonbill

Fig. 2-6. Box-and-arrow diagram for the list waders.

This list comprises three nodes, as illustrated by the diagram in Fig. 26. The first pointer in each node points to the element of the list that the node is holding; this element can be a number, a free variable name, a formula, or another node. In the case of waders, all three of these pointers point to the name of a different wading bird. In contrast, the second pointer in each node must point either to another node or to an empty list; we denote the latter case in a diagram by drawing a diagonal across the box in question. For instance, these pointers connect the three nodes in waders into a linear chain to which the list’s data elements are attached. We can create more complicated data structures by nesting lists inside other lists, but this only corresponds to a different arrangement in the underlying structure of linked nodes. For instance, the two-tiered list songbirds : [[cardinal, red], [warbler, yellow], [jay, blue]] $ corresponds to nine nodes as illustrated in Fig. 2-7. Note that each element in this list is itself a list consisting of the name of a songbird and its color. The primitive functions first() and rest() allow us to extract data from a structure of linked nodes. As their name indicate, these functions return the data indicated by the first node’s pointers. For example: first(waders); egret

164

2. COMPUTATION IN MATHEMATICS

songbirds

cardinal

red

warbler

yellow

jay

blue

Fig. 2-7. Box-and-arrow diagram for the two-tiered list songbirds.

rest(waders); [greatblueheron, spoonbill] first(songbirds); [cardinal, red] rest(songbirds); [[warbler, yellow], [jay, blue]] These functions can be combined to extract data buried deeper in a list as follows: first(rest(waders)); greatblueheron first(first(songbirds)); cardinal In fact, the indexing operator that we have already employed to select elements from a list works by repeatedly calling these two functions, in essence tracing its way down the chain of nodes to the desired element of the list. For instance, to find the third element in waders, waders[3]; spoonbill

2.4. LISTS AND OTHER DATA STRUCTURES

165

Maxima applies the rest() function twice followed by the first() function: first(rest(rest(waders))); spoonbill To access data in a multi-tiered list, we use multiple indices; thus, we can find the first element in the second list of songbirds by entering songbirds[2][1] warbler Again, this just calls a combination of the first() and rest() functions: first(first(rest(songbirds))); warbler We can implement our own version of this indexing process thanks to the following recursive formula ⎧ ⎨first(list) if n = 1, index(list, n) = ⎩index(rest(list), n − 1) otherwise. Our definition makes use of the predicates listp() and emptyp() to detect when the first input is not a list, or when that list is empty, returning an appropriate error message in each case: index(list, n) := block( /* test for base cases; otherwise apply recursion */ if not listp(list) then return("Error: input must be a list!") elseif emptyp(list) then return("Error: index is out of range!") elseif n = 1 then return(first(list)) else return(index(rest(list), n-1))) $ Note that unlike the case of an array, in which any element can be accessed in a constant amount of time, this function takes linear

166

2. COMPUTATION IN MATHEMATICS

time (proportional to n) to make its way down the chain of linked nodes to the nth element in the list. The longer access time does not arise from the particular recursive method described above, but from the underlying implementation of lists as sequences of nodes. It is critical to take this fact into consideration when determining an order of growth estimate for an algorithm. For example, we can show that the dominant factor in our implementation of Eratosthenes’ sieve is not the cancellation of multiples of primes, which indeed takes roughly n log (log(n)) steps, but the continual accessing of the sieve itself, which would take approximately n2 steps per prime. Another noteworthy consequence of Maxima’s implementation is that, as a time-saving measure, linked lists are not duplicated when you assign them to another variable or when you use them as inputs to a function. Instead, Maxima simply reuses the underlying list structure for the new variable or the function input. For instance, after you issue an assignment command like the one below, both of the variables birds and waders will point to the same linked list: birds : waders; [egret, greatblueheron, spoonbill] This means that changing the contents of one of these lists automatically changes the contents of the other: birds[2] : heron; heron birds; [egret, heron, spoonbill] waders; [egret, heron, spoonbill] If you wish to create a distinct, duplicate copy of a list, you should use the copylist() function as follows: birds : listcopy(waders); [egret, heron, spoonbill] This will point the two variables to identical, but separate copies of the list structure, so that changes to one of the lists will not affect the other:

2.4. LISTS AND OTHER DATA STRUCTURES

167

waders

ibis

egret

heron

spoonbill

Fig. 2-8. Box-and-arrow diagram for waders after a node is added by cons().

birds[2] : kestrel; kestrel birds; [egret, kestrel, spoonbill] waders; [egret, heron, spoonbill] It is generally considered bad form to define functions that modify the contents of their inputs. Such functions are said to be destructive because they can result in unintentional data corruption. If a function must modify its input lists, it is often safer to start by creating a duplicate copy on which to work. Indeed, as a matter of design, all of the functions in Table 2-3 are non-destructive. We can add a new node to the start of a list by calling on the primitive function cons(). For instance, by entering waders : cons(ibis, waders); [ibis, egret, heron, spoonbill] we instruct Maxima to construct a new node pointing to the name ibis and to the node at the start of waders. The result is the longer list illustrated by the diagram in Fig. 2-8. Observe that cons() does not create a new copy of waders, but simply reuses the structure of nodes that is already present in the computer’s memory, and therefore takes only constant time to complete. In contrast, the function endcons() must trace its way down

168

2. COMPUTATION IN MATHEMATICS

waders

ibis

egret

heron

spoonbill

stork

Fig. 2-9. Box-and-arrow diagram for waders after a node is added by endcons(). the chain of nodes before it can add a new node at the end of a list, as shown in the following: waders : endcons(stork, waders); [ibis, egret, heron, spoonbill, stork] The result is again a longer list, shown schematically in Fig. 2-9, but in this case the time it takes Maxima to place the last link in the chain is proportional to the length of the list. This means that endcons() is typically much slower than cons(). We can employ a strategy analogous to one used by the indexing function above to implement our own version of endcons() as follows: myendcons(x, list) := block( /* test for base case; otherwise apply recursion */ if not listp(list) then return("Error: input must be a list!") elseif emptyp(list) then return([x]) else return(cons(first(list), myendcons(x, rest(list))))) $ Note that the recursive step in this definition consists of applying the function being defined to the smaller list rest(list), thereby tracing our way down the chain of linked nodes until we reach an empty list. Two additional functions that follow a similar pattern are length(),

2.4. LISTS AND OTHER DATA STRUCTURES

169

which gives the number of elements in a list, length(waders); 5 length(songbirds); 3 and last(), which gives its last element, last(waders); stork last(songbirds); [jay, blue] Both of these functions can be implemented by tracing one’s way down the chain of nodes to the end of the list, on the one hand keeping track of the number of elements visited, and on the other simply waiting for the input to shrink to a one-element list. As expected, both of functions complete their task in linear time. There are three functions that perform a type of search on a list. The first of these is member(), a predicate that scans through a list to determine whether or not a given element forms part of the list: member(egret, waders); true member(mallard, waders); false Similarly, delete() scans a list for a given element and returns a copy of the list from which the element in question has been removed: delete(heron, waders); [ibis, egret, spoonbill, stork] Finally, assoc() does a sort of “directory look-up” by scanning a two-tiered list of ordered pairs, searching for an element among those appearing first in each pair, and then returning the corresponding second element or false if the search fails:

170

2. COMPUTATION IN MATHEMATICS

assoc(cardinal, songbirds); red assoc(oriole, songbirds); false We can implement our own version of this last function as follows: myassoc(x, list) := block( /* look in the first ordered pair else proceed to rest of list */ if not listp(list) then return("Error: input must be a list!") elseif emptyp(list) then return(false) elseif not listp(first(list)) or length(first(list)) # 2 then return("Error: input must be a list of pairs!") elseif first(first(list)) = x then return(first(rest(first(list)))) else return(myassoc(x, rest(list)))) $ In the worst case scenario, all three of the search functions operate in linear time. Maxima also provides two functions for merging the contents from two input lists. The first of these is join(), which creates a new list by dovetailing the elements of the two lists in alternating fashion, as when two decks of playing cards are shuffled. If the lists are not of the same length, then the extra elements in the longer list are ignored: join(waders, [coot, moorhen, anhinga]); [ibis, coot, egret, moorhen, heron, anhinga] The second merging function is append(), which returns a new list containing the elements of the first list followed by the elements of the second:

2.4. LISTS AND OTHER DATA STRUCTURES

171

append(waders, [coot, moorhen, anhinga]); [ibis, egret, heron, spoonbill, stork, coot, moorhen, anhinga] We can implement our own recursive version of append() as follows: myappend(L, M) := block( /* test for base case; otherwise apply recursion */ if not listp(L) or not listp(M) then return("Error: inputs must be lists!") elseif emptyp(L) then return(M) else cons(first(L), myappend(rest(L), M))) $ Note that this definition, which adds the elements from L to the front of M by calling cons(), runs in linear time. In contrast, an alternative approach that uses endcons() to add elements of M to the back of L would produce a much slower quadratic time function, even though the two definitions would look deceptively similar. Finally, Maxima provides three functions that rearrange the contents of a list. The first of these is flatten(), which takes a multitiered list and creates a single-tiered list containing all of the original data but without any of the internal brackets: flatten(songbirds); [cardinal, red, warbler, yellow, jay, blue] We can implement our own tree-recursive version of this function, flattening the first element of the list when needed, as in the following definition: myflatten(list) := block( /* if first item is itself a list, apply tree recursion otherwise, only linear recursion is needed */ if not listp(list) then return("Error: input must be a list!")

172

2. COMPUTATION IN MATHEMATICS

elseif emptyp(list) then return([]) elseif listp(first(list)) then append(myflatten(first(list)), myflatten(rest(list))) else cons(first(list), myflatten(rest(list))) $ Note that, unlike other tree-recursive functions that take an exponential number of steps to run, our function above runs in linear time, proportional to the total number of nodes representing the list. Next comes reverse(), which inverts the elements of a list: reverse(waders); [stork, spoonbill, heron, egret, ibis] This function can be implemented by a fairly straight forward iterative linear-time algorithm, taking elements from left to right in the input using the first() function and adding them from right to left to an empty list using the cons() function: myreverse(L) := block([M], M : [], /* take elements from left to right in list L and add them from right to left to list M */ if not listp(L) then return("Error: input must be a list!"), while (not emptyp(L)) do (M : cons(first(L), M), L : rest(L)), return(M)) $ The last function that will discuss here is sort(), which reorganizes the elements of a list in increasing order. Generally speaking, numbers are sorted by value, names are sorted alphabetically, and second-tier lists are sorted by their first element, as in the following

2.4. LISTS AND OTHER DATA STRUCTURES

173

examples: sort([2, 7, 1, 8, 2, 8, 1, 8, 2, 8, 4, 5, 9, 0, 4, 5]); [0, 1, 1, 2, 2, 2, 4, 4, 5, 5, 7, 8, 8, 8, 8, 9] sort(waders); [egret, heron, ibis, spoonbill, stork] sort(songbirds); [[cardinal,red],[jay,blue],[warbler,yellow]] It turns out that all of the algorithms involved in sorting a list efficiently (for there are several from which to choose) are surprisingly complicated.4 All we can say about it here is that an n-element list can be sorted in n log n time.

4 The interested reader should consult Volume 3 of Knuth [7].

Exercises for Section 2.4 1. The function length() returns the number of items in a given list: length([hawk, eagle, owl, vulture]); 4 Implement your own recursive version of this function by using a combination of the primitive functions first(), rest(), listp(), and emptyp(). Be sure to test that your implementation is working correctly. Hint: Remember that the length of the empty list is zero. 2. The function last() returns the last item in a given list: last([hawk, eagle, owl, vulture]); vulture Implement your own recursive version of this function. Hint: You can tell that a list A has a single element when rest(A) returns an empty list. What value should the output value of last(A) be in that case? 3. The predicate member() determines whether a given object is part of list: member(eagle, [hawk, eagle, owl, vulture]); true member(kite, [hawk, eagle, owl, vulture]); false Implement your own recursive version of this function. 4. The function delete() creates a new list by removing every instance of a given object from a given list: delete(goose, [duck, duck, goose]); [duck, duck] delete(duck, [duck, duck, goose]); [goose] Implement your own recursive version of this function. 5. The merging function join() creates a new list by dovetailing the elements of the two given lists in alternating fashion, ignoring any extra elements if either list is longer than the other:

EXERCISES

join([hawk, eagle, owl, vulture], [anhinga, bittern, cormorant]); [hawk, anhinga, eagle, bittern, owl, cormorant] Implement your own recursive version of this function.

175

2.5. Gaussian Elimination As we saw in Section 1.2, the Maxima command solve() can find the solution to a variety of different equations. It can also find the simultaneous solution to a system of equations in several unknowns, provided that there are as many equations as unknowns. In this situation, the individual equations that make up the system are grouped together using square brackets and commas, as in the following examples: solve([5*x = 3*y + 9, 7*y = 4*x + 2]); [[y = 2, x = 3]] solve([2*y - x = 3*x - 2, x^2 + 9 = 5*y - 7]); [[y = 5, x = 3], [y = 13, x = 7]] solve([2*x + 3*y + 4*z = 6, 3*x + 2*y + z = -1, 5*x + 4*y + 5*z = 3]); [[z = 1, y = 2, x = −2]] In each case, the result produced by solve() is structured as a list of solutions, each of which consists of a list of values surrounded by square brackets. Of particular interest to us here is the algorithm used to solve systems like the one in the last example: 2x + 3y + 4z = 6, 3x + 2y + z = −1, 5x + 4y + 5z = 3. Observe that this system consists of three linear equations. The left hand side of each equation is a linear combination of the unknowns, that is to say, each unknown appears only once, as a first power times an appropriate coefficient, added to the other unknowns. The right hand side consists of a single constant term. We can rewrite a system in this form by placing the coefficients on the left hand side inside a matrix , with each row corresponding to an equation and each column corresponding to an unknown, as in the following:

178

2. COMPUTATION IN MATHEMATICS

A: ⎡ 2 ⎢ ⎣3 5

matrix([2, 3, 4], [3, 2, 1], [5, 4, 5]); ⎤ 3 4 ⎥ 2 1⎦ 4 5

Since our specific matrix has three rows and three columns, we describe it as a 3 × 3 matrix; in general, when we are dealing with n equations in n unknowns, we will get an n × n matrix. Similarly, we write the constant terms on the right hand side of the equations as a column vector , in other words, as a matrix with a single column: B : matrix([6], [-1], [3]); ⎡ ⎤ 6 ⎢ ⎥ −1 ⎣ ⎦ 3 We can then recover our system of equations by multiplying the matrix of coefficients times a column vector of unknowns and setting it equal to our vector of constants: X : matrix([x], [y], [z]); ⎡ ⎤ x ⎢ ⎥ ⎣y ⎦ z A.X = B; ⎡ ⎤ ⎡ ⎤ 4z + 3y + 2x 6 ⎢ ⎥ ⎢ ⎥ ⎣ z + 2 y + 3 x ⎦ = ⎣−1⎦ 5z + 4y + 5x 3 Here, the product in question, denoted by a period (.) in Maxima, is a matrix multiplication. In the simplest of cases, it is just an application of the dot product from vector calculus. In particular, when computing the product of a single row and a single column, the entries from the row (read from left to right) are paired off with the entries from the column (read from top to bottom). The pairs are then multiplied together and the resulting products are added into a

2.5. GAUSSIAN ELIMINATION

179

grand total: ⎛

 p1

p2

p3

···

pn−2

pn−1

⎜ ⎜ ⎜  ⎜ ⎜ ⎜ pn · ⎜ ⎜ ⎜ ⎜ ⎜ ⎝

q1



q2 ⎟ ⎟ ⎟ q3 ⎟ n ⎟ .. ⎟  = pk · q k , ⎟ . ⎟ k=1 ⎟ qn−2 ⎟ ⎟ qn−1 ⎠ qn

Of course, for this product to make sense, the row and column vectors must contain the same number of entries. In general, the result of a matrix multiplication consists of a brand new matrix whose entries are the product of a row from the factor on the left times a column from the factor on the right. For example, if P is an m × k matrix and Q is a k × n matrix, then their product is defined as ⎡

p1 · q1

⎢ ⎢ p · q ⎢ 2 1 ⎢ ⎢ P · Q = ⎢ p3 · q1 ⎢ ⎢ .. ⎢ . ⎣ pm · q1

p1 · q2

p1 · q3

···

p2 · q2

p2 · q3

···

p3 · q2 .. .

p3 · q3 .. .

···

pm · q2

pm · q3

···

p1 · qn



⎥ p2 · qn ⎥ ⎥ ⎥ p3 · qn ⎥ ⎥, ⎥ .. ⎥ . ⎥ ⎦ pm · qn

where pi denotes the ith row in P and qj denotes the j th column in Q. Note that the number of rows in the left hand factor P must always agree with the number of columns in the right hand factor Q. This means that is it impossible to multiply the two matrices in the opposite order, unless we also have m = n. As with a Maxima list, we access the entries in a matrix by using the indexing operator [ · · · ]. In this case, however, two indices are used. The first index denotes the number of the row and the second the number of the column. For instance: A[2, 3]; 1

180

2. COMPUTATION IN MATHEMATICS

B[3, 1]; 3 We can also access an entire row at a time by using a single index as follows: A[1]; $ % 2 3 4 A common way to solve a system of linear equations is known as Gaussian elimination, named after the famous German mathematician Carl Friedrich Gauss (1777 – 1855). However, as is the case with many other algorithms, this approach has much earlier root, most notably in second century China. The first step in Gaussian elimination involves reducing an arbitrary system of equations into a simpler but equivalent system in upper triangular form, where the matrix of coefficients has only zeroes below the main diagonal. For instance, consider the following system, which is equivalent to the original system above: A: ⎡ 3 ⎢ ⎣0 0

matrix([3, 2, 1], [0, 2, 4], [0, 0, 2]); ⎤ 2 1 ⎥ 2 4⎦ 0 2

B : matrix([-1], [8], [2]); ⎡ ⎤ −1 ⎢ ⎥ ⎣8⎦ 2 A.X = B; ⎡ ⎤ ⎡ ⎤ z + 2y + 3x −1 ⎢ ⎥ ⎣ 4z + 2y ⎦ = ⎣ 8 ⎦ 2 2z Before explaining how this reduction is accomplished, let us discuss how this simpler system may be solved. The process is called back substitution. We start with a matrix in upper triangular form and

2.5. GAUSSIAN ELIMINATION

181

solve the equations from the bottom up. Each equation gives us the value of a new variable that can be used in turn to solve the equations above it. For example, we can solve the third equation above for z by dividing the constant term on the right by the coefficient on the left, as follows: z : B[3, 1]/A[3, 3]; 1 Next, we solve the second equation for y by subtracting the term involving z (which is no longer unknown) from the constant on the right and dividing by the appropriate coefficient from the left: y : (B[2, 1] - A[2, 3] * z)/A[2, 2]; 2 Finally, we solve the first equation for x by subtracting the terms involving y and z from the constant on the right and dividing by the coefficient from the left: x : (B[1, 1] - A[1, 2] * y - A[1, 3] * z)/A[1, 1]; −2 We can check that we have indeed solved our system of equations by replacing the variable matrix X with a matrix containing the values determined above, as follows: A.matrix([-2], [2], [1]) = B; ⎡ ⎤ ⎡ ⎤ −1 −1 ⎢ ⎥ ⎢ ⎥ ⎣ 8 ⎦=⎣ 8 ⎦ 2 2 kill(x, y, z) $ The following definition gives an implementation of the back substitution method described above for a given n × n upper triangular matrix A and an n × 1 column vector B. Our function starts with a call to zeromatrix(), creating a new n × 1 column vector X in which we will store the values of the unknowns; this allows us to solve systems with an arbitrary number of variables without having to give each one a separate name. Our function then runs through two nested for loops. The outer loop describes the process of solving for each unknown; the inner loop controls the repeated subtraction of the terms

182

2. COMPUTATION IN MATHEMATICS

involving the previously solved unknowns. backsubst(A, B) := block([n, X, i, j], n : length(A), X : zeromatrix(n, 1), /* back substitution assuming that A is an n by n matrix in upper triangular form */ for i : n thru 1 step -1 do (X[i, 1] : B[i, 1], for j : i+1 thru n do X[i, 1] : X[i, 1] - A[i, j] * X[j, 1], X[i, 1] : X[i, 1]/A[i, i]), return(X)) $ The process for transforming an arbitrary matrix into upper triangular form is known as row reduction. This procedure consists of the repeated application of row operations in which a multiple of one row of the matrix is added or subtracted from another row in order to eliminate the coefficients underneath the main diagonal. The same row operations are then applied to the column vector of constant terms, so that the resulting system is equivalent to the original system. For instance, let us consider our original system of equations: A: ⎡ 2 ⎢ ⎣3 5

matrix([2, 3, 4], [3, 2, 1], [5, 4, 5]); ⎤ 3 4 ⎥ 2 1⎦ 4 5

B : matrix([6], [-1], [3]); ⎡ ⎤ 6 ⎢ ⎥ ⎣−1⎦ 3

2.5. GAUSSIAN ELIMINATION

183

A.X = B; ⎡ ⎤ ⎡ ⎤ 4z + 3y + 2x 6 ⎢ ⎥ ⎣ z + 2 y + 3 x ⎦ = ⎣−1⎦ 3 5z + 4y + 5x We can perform row reduction “by hand” on a single augmented matrix obtained by joining A and B with the addcol() function as follows: M: ⎡ 2 ⎢ ⎣3 5

addcol(A, B); ⎤ 3 4 6 ⎥ 2 1 −1⎦ 4 5 3

First, we consider the entry on the top left of the matrix, which in this case is a 2; we shall refer to this entry as the pivot for the first step of the row reduction. We then modify the matrix by subtracting some multiple of the pivot row from the rows below it, so that every entry below the pivot becomes a zero. In each of these row operations, the multiple of the pivot to be taken away is found by dividing the entry to be eliminated by the value of the pivot. For example, in our particular case, we will subtract 32 times the pivot row to eliminate the 3 in the second row, and 52 times the pivot row to eliminate the 5 in the third row. The two row operations produce the following matrix: M[2] : M[2] M[3] : M[3] M; ⎡ 2 3 4 ⎢ 5 ⎣0 − 2 −5 0 − 72 −5

M[2, 1]/M[1, 1] * M[1] $ M[3, 1]/M[1, 1] * M[1] $ 6



⎥ −10⎦ −12

Next, we move to the entry in the second row and second column of the matrix, which in our example is − 52 ; this will become our next pivot. As before, we perform another row operation, this time subtracting − 75 times the pivot row from the third row in order to replace the entry below the pivot with a zero, as follows:

184

2. COMPUTATION IN MATHEMATICS

M[3] : M[3] - M[3, 2]/M[2, 2] * M[2] $ M; ⎡ ⎤ 2 3 4 6 ⎢ ⎥ ⎣0 − 52 −5 −10⎦ 0 0 2 2 We can now use the submatrix() function to split the augmented matrix and apply our back substitution algorithm to solve both our reduced and original systems: backsubst(submatrix(M, 4), submatrix(M, 1, 2, 3)); ⎡ ⎤ −2 ⎢ ⎥ ⎣2⎦ 1 The following definition implements the Gaussian elimination algorithm which, as illustrated by the example above, is the combination of row reduction followed by back substitution. The main body of our function consists of two nested for loops that will select the pivots and perform the required row operations. At each step of the process, we must ensure that our pivot is nonzero; otherwise, performing the associated row operations and back substitution will result in a division by zero. If a zero-valued pivot is detected, the main loop is interrupted by an internal return() statement and an error is flagged.5 On the other hand, if the entire row reduction is completed without encontering any zero-valued pivots, the results are passed on to the back substitution function backsubst(). Gauss(A, B) := block([n, i, j, pivot, mult], n : length(A), A : copymatrix(A), B : copymatrix(B), /* perform row reductions */ for i : 1 thru n do 5 Remember that a return() statement inside a loop will not interrupt the function

containing it.

2.5. GAUSSIAN ELIMINATION

185

(pivot : A[i, i], if pivot = 0 then return(), for j : i+1 thru n do (mult : A[j, i]/pivot, A[j] : A[j] - mult * A[i], B[j] : B[j] - mult * B[i])), print("Reduced form: ", A, B), /* back substitute unless zero pivot is found */ if pivot = 0 then return("Error: Pivot equal to zero!") else return(backsubst(A, B))) $ There is one additional detail about the definition above that requires explanation. According to the usual rules of dynamic scoping, the inputs A and B are part of our function’s local environment. However, they may still point to a matrix that is part of the global environment; this is typically the case when our inputs are lists or matrices that have already been assigned to global variables. To guarantee a non-destructive implementation, we begin by calling the function copymatrix() to create local copies of these matrices, so that the global versions are not unintentionally modified. Our implementation works well for systems of any size as long as only nonzero pivots are encountered, as in our original example: A.X ⎡ = B; ⎤ ⎡ ⎤ 4z + 3y + 2x 6 ⎢ ⎥ ⎣ z + 2 y + 3 x ⎦ = ⎣−1⎦ 3 5z + 4y + 5x Gauss(A, B); ⎡

2

3

⎢ Reduced form: ⎣0 − 52 0 0

4

⎤⎡

6



⎥⎢ ⎥ −5⎦ ⎣−10⎦ 2 2

186

2. COMPUTATION IN MATHEMATICS

⎤ −2 ⎣2⎦ 1 ⎡

On the other hand, if a zero pivot arises, our function announces an error: A: ⎡ 1 ⎢ ⎣2 5

matrix([1, 2, 3], [2, 4, 4], [5, 4, 5]); ⎤ 2 3 ⎥ 4 4⎦ 4 5

B : matrix([1], [-2], [3]); ⎡ ⎤ 1 ⎢ ⎥ ⎣−2⎦ 3 A.X = B; ⎡ ⎤ ⎡ ⎤ 3z + 2y + x 1 ⎢ ⎥ ⎢ ⎥ ⎣−4 z + 4 y + 2 x⎦ = ⎣−2⎦ 5z + 4y + 5x 3 Gauss(A, B);



1

2

⎢ Reduced form: ⎣0 0 0 −6

3

⎤⎡

1



⎥⎢ ⎥ −2 ⎦ ⎣−4⎦ −10 −2

Error: Pivot equal to zero! The appearance of zero-valued pivots is not necessarily an indication that a given system is insoluble. For instance, the problem with the system above is that the first set of row operations produced a zero-valued pivot in the second row. However, in this particular case, this is not an insurmountable problem since there is a perfectly good candidate to serve as the next pivot in the third row. Therefore, all that is required is for us to call on the function rowswap() to switch

2.5. GAUSSIAN ELIMINATION

187

the second and third rows of our system. Then, trying Gaussian elimination again will produce a valid solution: A: ⎡ 1 ⎢ ⎣5 2

rowswap(A, 2, 3); ⎤ 2 3 ⎥ 4 5⎦ 4 4

B : rowswap(B, 2, 3); ⎡ ⎤ 1 ⎢ ⎥ ⎣3⎦ −2 Gauss(A, B);



1

2

⎢ Reduced form: ⎣0 −6 0 0 ⎡ ⎤ 1 ⎢ ⎥ −3 ⎣ ⎦ 2

3

⎤⎡

1



⎥⎢ ⎥ −10⎦ ⎣−2⎦ −2 −4

Unfortunately, it is not always possible to solve a system by swapping rows as above. Furthermore, we would like to automate the way in which we deal with this type of exceptional cases. In the next section, we will explore an alternate way to implement row reduction that avoids zero-valued pivots whenever possible, and thus eliminates the need for special treatment of difficult cases like the example above.

Exercises for Section 2.5 1. Define a function that computes the dot product of two vectors without using the matrix algebra machinery built into Maxima. In particular, your function should take two lists of the form P = [p1 , p2 , p3 , . . . , pn ] and Q = [q1 , q2 , q3 , . . . , qn ] as inputs and return as output their dot product n  dotproduct(P, Q) = p k · qk . k=1

2. Define a function that computes the product of two matrices without using the matrix algebra machinery built into Maxima. In particular, your function should take as input a pair of two-tiered lists of the form [[p1,1 , p1,2 , . . . , p1,k ], [p2,1 , p2,2 , . . . , p2,k ], . . . , [pm,1 , pm,2 , . . . , pm,k ]] and [[q1,1 , q1,2 , . . . , q1,n ], [q2,1 , q2,2 , . . . , q2,n ], . . . , [qk,1 , qk,2 , . . . , qk,n ]] representing an m × k matrix P and a k × n matrix Q, and return as output a similar two-tiered list representing the product P · Q. 3. Define a predicate function that checks whether a given n × n matrix is in upper triangular form and returns an output value of true or false accordingly. 4. Consider the following matrix-based method for finding Fibonacci numbers: Start with the column vector   1 F1 = , 0 and compute the first few terms in the sequence defined by the recursive formula   1 1 Fk+1 = · Fk . 1 0 The nth term in this sequence will be a column vector comprising two consecutive Fibonacci numbers:   Fn Fn = . Fn−1 Implement a function that, given a value of n, performs the appropriate matrix multiplications and returns the value of the nth Fibonacci number Fn .

EXERCISES

5.

189

(a) How many row operations are required to row reduce an n × n matrix? (b) How many multiplications and/or divisions does a single row operation on an n × n matrix require? Remember to count every time that an entire row gets multiplied as n distinct multiplications. (c) How many multiplications and/or divisions are required to solve an n × n matrix in upper triangular form using back substitution? (d) Explain why your answers to parts (a) through (c) show that the Gaussian elimination algorithm runs in cubic time.

2.6. Partial Pivoting In the last section, we discussed a simple implementation of Gaussian elimination that considered pivots in order, starting with the first row and moving down to the bottom row of a system. As we observed, this somewhat na¨ıve approach resulted in errors whenever a zero-valued pivot was encountered, even though an alternative choice of pivot was available. Out initial attempt to overcome this difficulty was to switch the order of the rows in the system, but it is difficult to see how this technique could be automated by Maxima. Furthermore, in practice, it is not efficient to actually swap rows like we did. Instead, it is much better to invest a little effort in bookkeeping so that we can proceed with the selection of pivots in an order that avoids zero-valued pivots whenever possible. This will be the goal of this section. The main idea that we shall explore consists of choosing pivots according to some alternate permutation of rows, rather than proceeding in order from top to bottom. For example, consider the following system of equations: A : matrix([0, 3, 4], [0, 0, -1], [5, 4, 5]) $ X : matrix([x], [y], [z]) $ B : matrix([5], [-2], [-4]) $ A.X = B; ⎡ ⎤ ⎡ ⎤ 4z + 3y 5 ⎢ ⎥ ⎢ ⎥ −z ⎣ ⎦ = ⎣−2⎦ 5z + 4y + 5x −4 This system is not in upper triangular form, but it can easily be put in that form by swapping the rows. Alternatively, we can think of this matrix as the result of a row reduction in which the pivots were selected in a different order, with the first pivot taken from the third row, the second pivot from the first row, and the third pivot from the second row. This pivot-row correspondence is precisely the sort of bookkeeping information needed to complete the back substitution

192

2. COMPUTATION IN MATHEMATICS

for this system. We shall encode it by the following permutation list: P : [3, 1, 2]; [3, 1, 2] According to this permutation list, in order to apply back substitution, we should solve for z using the second row, then solve for y using the first row, and finish by solving for x using the third row. In other words, we should read the values in the permutation list from right to left to determine which row to use in each step of the back substitution. The following modification to our earlier implementation, which replaces the pivot’s row index with the new variable ipivot, allows us to do this: backsubst(A, B, P) := block([n, X, i, j, ipivot], n : length(A), X : zeromatrix(n, 1), /* back substitution assuming that A is an n by n matrix in permuted upper triangular form corresponding to the permutation list P */ for i : n thru 1 step -1 do (ipivot : P[i], X[i, 1] : B[ipivot, 1], for j : i+1 thru n do X[i, 1] : X[i, 1] - A[ipivot, j] * X[j, 1], X[i, 1] : X[i, 1]/A[ipivot, i]), return(X)) $ backsubst(A, B, P); ⎡ ⎤ −2 ⎢ ⎥ ⎣−1⎦ 2

2.6. PARTIAL PIVOTING

193

A.% = B; ⎡ ⎤ ⎡ ⎤ 5 5 ⎢ ⎥ ⎢ ⎥ ⎣−2⎦ = ⎣−2⎦ −4 −4 Before we can implement the corresponding version of the row reduction process, we must answer two questions: (a) how we should select pivots, and (b) how we can determine the corresponding permutation list. Perhaps the simplest and most advantageous way to pick pivots is to select from among all remaining rows the pivot with the largest absolute value. This approach is known as partial pivoting and it has two advantages over our earlier implementation. First of all, it clearly eliminates zero-valued pivots whenever possible. Secondly, if we are computing with floating-point numbers, it reduces the effects of roundoff error that can occur when dividing by too small a pivot during a row operation. As we select pivots, we shall compute the permutation list by recording which pivot comes from which row. In particular, at each stage of the process, the start of the permutation list will indicate the rows from which a pivot has already been extracted, while the tail end will record the remaining rows from which a pivot has yet to be taken and on which row operations are to be performed. For instance, after choosing the first i − 1 pivots, the permutation list P will look something like this:

p1

p2

...

pi−1

rows from which a pivot has already been extracted

pi

pi+1

...

pn

remaining rows on which row operations will be performed

To illustrate the process of partial pivoting, let us consider the augmented matrix representing our original system of equations from Section 2.5: A : matrix([2, 3, 4], [3, 2, 1], [5, 4, 5]) $ B : matrix([6], [-1], [3]) $

194

2. COMPUTATION IN MATHEMATICS

M: ⎡ 2 ⎢ ⎣3 5

addcol(A, B); ⎤ 3 4 6 ⎥ 2 1 −1⎦ 4 5 3

At the outset, the permutation list P reflects the standard order of pivoting: 1

2

3

Now, observe that the coefficient with largest absolute value in the first column of our augmented matrix is the 5 located in the third row. This will be our first pivot, so we swap the first and third entries in our permutation list (thus moving the 3 into the first position) to mark the location of the first pivot: 3

2

1

We then perform the appropriate row operations (as indicated by the dark boxes) in the same order that they appear in the permutation list P: M[2] : M[2] - M[2, 1]/M[3, 1] * M[3] $ M[1] : M[1] - M[1, 1]/M[3, 1] * M[3] $ M; ⎤ ⎡ 7 24 2 0 5 5 ⎥ ⎢ ⎣0 − 25 −2 − 14 5 ⎦ 5 4 5 3 Next, we look at the second column of coefficients in the remaining rows of our matrix (again, indicated by the dark boxes in the permutation list). This time, the entry with the largest absolute value is the 7 5 in the first row, so this becomes our new pivot. Hence, we swap the second and third entries in the permutation list (moving the 1 into the second position) to mark the location of the second pivot: 3

1

2

2.6. PARTIAL PIVOTING

195

Then, we perform the last row operation as indicated by the last dark box in the permutation list: M[2] : M[2] - M[2,2]/M[1,2] * M[1] $ M; ⎡ ⎤ 24 0 75 2 5 ⎢ ⎥ − 10 ⎣0 0 − 10 7 7 ⎦ 5 4 5 3 As expected, this leaves us with a permuted upper triangular matrix that we can solve using the back substitution function defined above: backsubst(submatrix(M, 4), submatrix(M, 1, 2, 3), [3, 1, 2]); ⎡ ⎤ −2 ⎢ ⎥ ⎣2⎦ 1 The following definition implements the partial pivoting scheme by selecting the ith pivot as described above. The search for this pivot relies on the following three local variables: ◦ index points to a location in the permutation list P, ◦ row is the corresponding row number in the matrix A, and ◦ value is the absolute value of the corresponding coefficient in the matrix A. These variables are initialized with the values corresponding to the ith entry in P, and are then modified as better choices for a pivot are discovered by the for loop. When the loop runs its course and a pivot is selected, the permutation list is modified by swapping the appropriate pair of entries so as to mark the location of the new pivot. partpivot(A, P, i) := block([n, index, row, value, k], n : length(A), index : i, row : P[i], value : abs(A[row, i]), /* assume that P[1], P[2], ..., P[i-1] denote the previous pivots for A

196

2. COMPUTATION IN MATHEMATICS

while P[i], P[i+1], ..., P[n] denote the candidates for next pivot */ for k : i+1 thru n do if abs(A[P[k], i]) > value then (index : k, row : P[k], value : abs(A[row, i])), /* swap values in permutation list P; modifies P in the process */ if index # i then (P[index] : P[i], P[i] : row), return(row)) $ Note that this function typically changes the value of the permutation list P stored in memory (according to the rules of dynamic scoping). For instance, consider what happens when we search for the first pivot in the example above: P : [1, 2, 3]; [1, 2, 3] partpivot(A, P, 1); 3 P; [3, 2, 1] This side effect will turn out to be crucial as we combine the partial pivoting scheme with the row reduction process. The complete implementation of a general Gaussian elimination algorithm with partial pivoting is accomplished by three modifications to our earlier version of the function Gauss(): ◦ the permutation list P is constructed by calling the command makelist(),

2.6. PARTIAL PIVOTING

197

◦ the pivot’s row index is replaced by the new variable ipivot, which gets its value from the partpivot() function defined above, and ◦ each row operation is indexed by P[j] rather than j. Our new function definition looks like this: Gauss(A, B) := block([n, i, j, pivot, mult, P, ipivot], n : length(A), A : copymatrix(A), B : copymatrix(B), P : makelist(i, i, 1, n), /* a zero-valued pivot indicates a singular matrix */ for i : 1 thru n do (ipivot : partpivot(A, P, i), pivot : A[ipivot, i], if pivot = 0 then return(), for j : i+1 thru n do (mult : A[P[j], i]/pivot, A[P[j]] : A[P[j]] - mult * A[ipivot], B[P[j]] : B[P[j]] - mult * B[ipivot])), print("Reduced form: ", A, B), if pivot = 0 then return("Error: Pivot equal to zero!") else return(backsubst(A, B, P))) $ This new version of Gaussian elimination finds the same solution to our original system of equations as before, but note that it goes through a different reduced form because of the partial pivoting: A.X = B; ⎡ ⎤ ⎡ ⎤ 4z + 3y + 2x 6 ⎢ ⎥ ⎢ ⎥ ⎣ z + 2 y + 3 x ⎦ = ⎣−1⎦ 5z + 4y + 5x 3

198

2. COMPUTATION IN MATHEMATICS

Gauss(A, B);



0

⎢ Reduced form: ⎣0 5 ⎡ ⎤ −2 ⎢ ⎥ ⎣2⎦ 1

7 5

0 4

⎤⎡



24 5 ⎥ ⎢ 10 ⎥ − 10 − ⎦ ⎣ 7 7 ⎦

2

5

3

Our new function can also solve systems that would otherwise cause problems for our earlier “top-to-bottom” pivoting implementation: A : matrix([1, 2, 3], [2, 4, 4], [5, 4, 5]) $ B : matrix([1], [-2], [3]) $ A.X = B; ⎡ ⎤ ⎡ ⎤ 3z + 2y + x 1 ⎢ ⎥ ⎢ ⎥ ⎣−4 z + 4 y + 2 x⎦ = ⎣−2⎦ 5z + 4y + 5x 3 Gauss(A, B);



0

⎢ Reduced form: ⎣0 5 ⎡ ⎤ 1 ⎢ ⎥ ⎣−3⎦ 2

0 12 5

4

1

⎤⎡

2



⎥ ⎥⎢ 2⎦ ⎣− 16 5 ⎦ 3 5

It must be noted that even partial pivoting does not completely eliminate the possibility of encountering a zero-valued pivot. In particular, there are matrices of coefficients that always result in a zerovalued pivot, regardless of how the pivots are selected. Such matrices are said to be singular since they represent an exception to the usual rule of thumb that says that a linear system with the same number of equations as unknowns has a unique solution. For instance, consider the following system:

2.6. PARTIAL PIVOTING

199

A : matrix([4, -5, 6], [2, 3, 2], [3, -1, 4]) $ B : matrix([5], [1], [2]) $ A.X = B; ⎡ ⎤ ⎡ ⎤ 6z − 5y + 4x 5 ⎢ ⎥ ⎢ ⎥ ⎣ 2 z + 3 y + 2 x ⎦ = ⎣ 1⎦ 4z − y + 3x 2 Gauss(A, B);



4 −5

⎢ Reduced form: ⎣0 0

11 2

0

6

⎤⎡

5



⎥⎢ ⎥ −1⎦ ⎣− 32 ⎦ −1 0

Error: Pivot equal to zero! The last row in the reduced system translates to the equation 0 · x + 0 · y + 0 · z = −1. Evidently, neither this equation nor the system that produced it has any solutions: solve([4*x - 5*y + 6*z = 5, 2*x + 3*y + 2*z = 1, 3*x - y + 4*z = 2]); [] In contrast, the same singular coefficient matrix with a different set of constants gives a different sort of result: A : matrix([4, -5, 6], [2, 3, 2], [3, -1, 4]) $ B : matrix([3], [1], [2]) $ A.X = B; ⎡ ⎤ ⎡ ⎤ 6z − 5y + 4x 3 ⎢ ⎥ ⎢ ⎥ ⎣ 2 z + 3 y + 2 x ⎦ = ⎣ 1⎦ 4z − y + 3x 2

200

2. COMPUTATION IN MATHEMATICS

Gauss(A, B);



4 −5

⎢ Reduced form: ⎣0 0

11 2

0

6

⎤⎡

3



⎥⎢ ⎥ −1⎦ ⎣− 12 ⎦ 0 0

Error: Pivot equal to zero! In this case, the row of the row-reduced system says that 0 · x + 0 · y + 0 · z = 0, which is always true. Consequently, this system of equations has an infinite number of solutions. In fact, the solve() command gives formulas for two of the unknowns in terms of the third, which it treats as the free variable %r1: solve([4*x - 5*y + 6*z = 3, 2*x + 3*y + 2*z = 1, 3*x - y + 4*z = 2]); solve: dependent equations eliminated: (3) %r1 11 %r1 − 7 ,y = − , x = %r1]] [[z = − 14 7 These examples are typical of singular matrices. When these matrices define a system of linear equations, the result is either an inconsistent system with no solution or an under-determined system with infinitely many solutions. The good news, however, is that singular matrices are relatively rare among square matrices (even though there are infinitely many of them), and they can be detected by a variety of methods from linear algebra.

Exercises for Section 2.6 1. The Maxima function determinant() computes a numerical measure indicating roughly how far a square matrix is from being singular. In particular, a matrix is singular if and only if its determinant is zero. For instance: determinant(matrix([2, 3, 4], [3, 2, 1], [5, 4, 5])); −10 determinant(matrix([1, 2, 3], [2, 4, 4], [5, 4, 5])); −12 determinant(matrix([4, -5, 6], [2, 3, 2], [3, -1, 4])); 0 The following recursive rule gives one way to define the determinant of an n × n matrix A: ⎧ ⎪ ⎪ if n = 1, ⎪ ⎨a(1,1) det(A) =

 ⎪ k+1 ⎪ a(1,k) det(submatrix(1, A, k)) otherwise. ⎪ ⎩ (−1) n

k=1

Here submatrix(1, A, k) is the result of deleting the first row and kth column in A. Implement your own tree-recursive version of the determinant function based on this formula. Be sure to test that your function gives the correct determinant for the three matrices above. 2. The recursive formula in Exercise 1 is terribly inefficient since it requires more than n! multiplications to complete. The following pseudocode, based on Gaussian elimination, gives a better cubictime algorithm for computing the determinant of a matrix: determinant(A): x Let n : length(A), P : [1, 2, . . . , n], d : 1, and i : 1. y Follow the partial pivoting scheme to find the next pivot pi in the matrix A. z If the choice of pivot results in a change in the permutation list P, set d : −d · pi ; otherwise set d : d · pi . { If pi = 0, then go to step ~. | Perform the appropriate row operations on the matrix A. } Set i : i + 1 and repeat steps y through | until i > n. ~ Return d.

202

2. COMPUTATION IN MATHEMATICS

Implement the algorithm above by making suitable modifications to the Gauss() and partpivot() functions defined in this section. Be sure to test that your function gives the correct determinant for the three matrices in Exercise 1. 3. Every singular n × n matrix A has the property that its rows and its columns are linearly dependent, that is, they may be combined linearly to form the zero vector in a nontrivial way. For example, consider the specific case of the singular matrix ⎡ ⎤ 4 −5 6 ⎢ ⎥ A = ⎣2 3 2⎦ . 3

−1

4

(a) Find three real numbers x1 , x2 , and x3 , not all of which are zero, such that         x1 · 4 −5 6 + x2 · 2 3 2 + x3 · 3 −1 4 = 0 0 0 . Interpret this result as a matrix multiplication of the form X · A = 0, where X is the nonzero row vector comprising x1 , x2 , and x3 . Hint: Observe that when we row reduced this matrix, we obtained a row vector with all zeroes. What were the row operation multipliers that produced this result? (b) Find three real numbers y1 , y2 , and y3 , not all of which are zero, such that ⎡ ⎤ ⎡ ⎤ ⎡ ⎤ ⎡ ⎤ 4 −5 6 0 ⎢ ⎥ ⎢ ⎥ ⎢ ⎥ ⎢ ⎥ y 1 · ⎣2 ⎦ + y 2 · ⎣ 3 ⎦ + y 3 · ⎣ 2 ⎦ = ⎣ 0 ⎦ . 3 −1 4 0 Interpret this result as a matrix multiplication of the form A · Y = 0, where Y is the nonzero column vector comprising y1 , y2 , and y3 . Hint: Consider a column reduction algorithm that applies column operations to this matrix. 4. The information gathered by the row reduction algorithm can be used to factor any non-singular n × n matrix A into a product of the form A = Q · L · U, where U is upper triangular, L is lower triangular, and Q is a permutation matrix. For instance, the row reductions in page 197 produce the following factorizations:

EXERCISES

Q : matrix([0,1,0], [0,0,1], [1,0,0]); U : matrix([5, 4, 5], [0, 7/5, 2], [0, 0, -10/7]); L : matrix([1, 0, 0], [2/5, 1, 0], [3/5, -2/7, 1]); Q.L.U; ⎡ ⎤ 0 1 0 ⎢ ⎥ ⎣0 0 1 ⎦ 1 0 0 ⎤ ⎡ 5 4 5 ⎥ ⎢ 7 2 ⎦ ⎣0 5 10 0 0 −7 ⎤ ⎡ 1 0 0 ⎥ ⎢2 1 0⎦ ⎣5 3 2 −7 1 5 ⎡ ⎤ 2 3 4 ⎢ ⎥ ⎣3 2 1 ⎦ 5 4 5 Q : matrix([0,0,1], [0,1,0], [1,0,0]); U : matrix([5, 4, 5], [0, 12/5, 2], [0, 0, 1]); L : matrix([1, 0, 0], [2/5, 1, 0], [1/5, 1/2, 1]); Q.L.U; ⎡ ⎤ 0 0 1 ⎢ ⎥ ⎣0 1 0 ⎦ 1 0 0 ⎤ ⎡ 5 4 5 ⎥ ⎢ 12 ⎣0 5 2 ⎦ 0 0 1 ⎤ ⎡ 1 0 0 ⎥ ⎢2 ⎣ 5 1 0⎦ 1 1 1 5 2 ⎡ ⎤ 1 2 3 ⎢ ⎥ ⎣2 4 4 ⎦ 5 4 5

203

204

2. COMPUTATION IN MATHEMATICS

(a) The permutation matrix Q is obtained directly from the permutation list P by starting with a matrix that initially contains only zeroes, and placing a one in the P[j]th row and j th column for each j ∈ {1, . . . , n}. (You can use the zeromatrix() function to create this matrix.) Define a function that takes a permutation list P and returns the corresponding permutation matrix Q. (b) The upper triangular matrix U is obtained by swapping the rows of the reduced form matrix produced by the algorithm according to the order given by the permutation list P. Define a function that takes a reduced form matrix and its associated permutation list P, and returns the corresponding upper triangular matrix U . (c) The lower triangular matrix L is obtained by placing the multipliers used for the various row operations in a matrix consisting of all zeroes except for a main diagonal of ones. Our particular implementation stores these multipliers one at a time in the local variable mult. Modify the definition of Gauss() so the multiplier used for the j th row operation by the ith pivot is also stored in the P[j]th row and ith column of an accumulator matrix M , initially containing all zeroes. Then, define a function that takes the final version of the accumulator matrix M and the permutation list P, and produces the corresponding lower triangular matrix L by swapping the rows of M according to the order given by P and adding ones along the diagonal. (d) Define a function QLUfactor() that takes a singular matrix A and performs the row reduction algorithm. Your function should then compute and print the matrices Q, L, and U , and verify that their product is equal to A. 5. The Maxima function invert() computes the inverse of a nonsingular square matrix P , that is, a second square matrix Q for which the products P · Q and Q · P are both equal to the identity matrix consisting of all zeroes except for a diagonal of ones. For instance: P : matrix([2, 3, 4], [3, 2, 1], [5, 4, 5]); ⎡ ⎤ 2 3 4 ⎢ ⎥ ⎣3 2 1 ⎦ 5 4 5

EXERCISES

205

Q : invert(P); ⎡ 3 ⎤ 1 1 − 5 − 10 2 ⎢ ⎥ 1 −1⎦ ⎣ 1 − 15

7 − 10

P.Q; ⎡ 1 0 ⎢ ⎣0 1

0

0

0

Q.P; ⎡ 1 0 ⎢ ⎣0 1 0 0

1 2



⎥ 0⎦ 1

0



⎥ 0⎦ 1

Define your own version of this function by modifying the Gaussian elimination algorithm so that it performs identical row operations on the given matrix P as well as a second matrix Q, initially set to the identity matrix ident(n). If a zero-valued pivot is encountered, then P is singular and has no inverse; in that case, your function should return an error message. On the other hand, if P can be successfully reduced to permuted upper triangular form, then your function should perform a “row-wise” back substitution on Q, thus finding the entries of the inverse matrix one row at a time.

2.7. Binary Codes At its most basic level, the operation of a computer consists in the manipulation of binary digits, or bits, which are manifested physically within the circuits of the computer as differences in voltage. A low voltage (indicating that some switch inside the computer has been turned to the “off” position) is denoted as a zero, while a high voltage (indicating that a switch has been turned to the “on” position) is denoted as a one. These zeroes and ones can be combined in a variety of ways to form binary codes representing numbers, alphanumeric characters, and other abstract objects. Huge numbers of these bits are transmitted, copied, compared, and modified at incredible speeds in order to produce the seemingly intelligent interactions that we have come to expect from our computers. For example, when you press a key in your keyboard, a sequence of zeroes and ones is sent through the cable (or perhaps through the wireless router) connecting the keyboard to your computer’s central processing unit. This binary code tells the computer which key you pressed. More than likely, your computer uses some variation of the ASCII code developed back in the 1960’s by the American Standards Association. This code assigns every letter, numeral, and symbol in the keyboard to some eight-bit sequence. Thus, the uppercase “J” is assigned to the binary sequence 01001010, the lowercase “c” to 01100011, the numeral 5 to 00110101, the question mark to 00111111, and so on. The way that the ASCII code assigns binary sequences to the characters in the keyboard is essentially arbitrary. Certainly, the numerals, the uppercase letters, and the lowercase letters are respectively grouped together in order, but there is not much more that can be discerned from these codes. On the other hand, numbers are encoded in binary by a systematic method that reflects their mathematical and computational nature. This means that a computer performs arithmetic operations on numbers by mimicking these operations on the corresponding binary codes. In other words, when we ask Maxima to evaluate an expression like 139 + 346; 485 what actually happens in the deep recesses of our computer’s circuitry

208

2. COMPUTATION IN MATHEMATICS

is that each of the numbers 139 and 346 is encoded into a binary sequence, and that these sequences are then combined to produce a third sequence representing the final result of 485. The most common approach for encoding a nonnegative integer in binary is to consider writing the integer as a sum of distinct powers of two. For instance, we might write 139 = 128 + 8 + 2 + 1 = 27 + 23 + 21 + 20 and 346 = 256 + 64 + 16 + 8 + 2 = 28 + 26 + 24 + 23 + 21 . Suppose that we list the various powers of two in decreasing order, as we did in the examples above. Then we can produce a binary sequence representing the integer in question by replacing every power of two that appears in the sum with a one, and introducing a zero wherever a power of two is missing. In other words, suppose that we can express an integer n as a sum of the form n=

k 

2i bi = 2k bk + 2k−1 bk−1 + 2k−2 bk−2 + · · · + 22 b2 + 2 b1 + b0 ,

i=0

where each of the coefficients bi is either a one (indicating that the matching power of two appears in the sum) or a zero (indicating that it is missing from the sum). Then we will represent this integer by the binary sequence n



bk bk−1 bk−2 . . . b2 b1 b0 .

According to this recipe, the integers 139 and 346 are assigned to the following binary codes: 139



0010001011,

346



0101011010.

Take a moment to observe the correspondence between the ones and zeroes in each of these codes and the powers of two present or absent in the sums above. Furthermore, note that we added some extra leading zeroes to the binary codes above to make both of them ten bits long. We can disregard these leading zeroes, since they do not affect the value corresponding to a binary code. We will now create a “working model” to explore how these binary sequences are used to implement the operations of integer arithmetic. To be more specific, we will represent each binary code as a list of zeroes and ones as follows:

2.7. BINARY CODES

209

A : [0, 0, 1, 0, 0, 0, 1, 0, 1, 1]; [0, 0, 1, 0, 0, 0, 1, 0, 1, 1] B : [0, 1, 0, 1, 0, 1, 1, 0, 1, 0]; [0, 1, 0, 1, 0, 1, 1, 0, 1, 0] We begin our working model by designing a function that converts any nonnegative integer into its corresponding binary code. The main idea is to imagine expressing a given integer n as a sum of distinct powers of two, say as n = 2k bk + 2k−1 bk−1 + 2k−2 bk−2 + · · · + 23 b3 + 22 b2 + 2 b1 + b0 , where each coefficient bi is either a zero or a one. We will determine the binary code corresponding to n by extracting the values of these coefficients one at a time from right to left. With this aim in mind, observe that with the possible exception of b0 , every term in this sum is even. This means that the parity of the integer n (that is, whether it is even or odd) is completely determined by the value of b0 . In particular, we have  0 if and only if n is even, b0 = 1 if and only if n is odd. If we subtract b0 from n and divide the result by two, we obtain another integer x1 = 12 (n−b0 ) = 2k−1 bk +2k−2 bk−1 +2k−3 bk−2 +· · ·+22 b3 +2 b2 +b1 . As before, each term in this sum is even, except possibly for b1 . Consequently, the parity of the integer x1 is controlled by the value of b1 , so  0 if and only if x1 is even, b1 = 1 if and only if x1 is odd. Similarly, we find the value of b2 by subtracting b1 away from x1 and dividing by two: x2 = 12 (x1 −b1 ) = 2k−2 bk +2k−3 bk−1 +2k−4 bk−2 +· · ·+22 b4 +2 b3 +b2 .

210

2. COMPUTATION IN MATHEMATICS

Once again, the result will be an integer whose parity is dictated by b2 . Thus,  0 if and only if x2 is even, b2 = 1 if and only if x2 is odd. We can discern the value of all the other bits in our binary sequence successively, from right to left, by iterating the same process over and over again. Namely, we shall: x determine the value of the new bit bi :  0 if and only if xi is even, bi = 1 if and only if xi is odd; y subtract the value of bi from xi ; z divide the result by two and set xi+1 = 12 (xi − bi ). Eventually, we will whittle our original integer down to zero and the whole process can come to a stop. For example, Fig. 2-10 shows an outline of this encoding process applied to the integer 139. The general approach described above can be implemented as a Maxima function as follows. We begin with our input n (an integer) and three local variables: ◦ x is initially a copy of the value of n that we will modify later on, ◦ b holds the value of each bit extracted from n, and ◦ A contains a list (initially empty) of zeroes and ones. We will first determine the parity of x by calling on the function mod(). As you will recall, this function gives the remainder produced when one integer is divided into another. In particular, we will decide whether x is even or odd by evaluating the expression mod(x, 2). If the result of this expression is zero, then x is even; if it is one, then x is odd. Note that this coincides with the value of the next bit b in our binary sequence. Thus, we will append this bit onto the left side of A using the concatenation function cons(). Finally, we will subtract the value of b from x and divide by two, storing the result as our new value of x. We will then repeat the whole process using this new value of x, until it is reduced down to zero. By this point, A will contain the entire binary code for n, so we will finish off our function by returning this result. This encoding procedure is captured by the following definition:

2.7. BINARY CODES

n = 139 ↓   1 x1 = 2 139 − 1 = 69 ↓   1 x2 = 2 69 − 1 = 34 ↓   1 x3 = 2 34 − 0 = 17 ↓   1 x4 = 2 17 − 1 = 8 ↓   1 x5 = 2 8 − 0 = 4 ↓   1 x6 = 2 4 − 0 = 2 ↓   1 x7 = 2 2 − 0 = 1 ↓   1 x8 = 2 1 − 1 = 0

odd

−−−→ odd

−−−→ even

−−−→ odd

−−−→ even

−−−→ even

−−−→ even

−−−→ odd

−−−→ even

−−−→

211

b0 = 1 b1 = 1 b2 = 0 b3 = 1 b4 = 0 b5 = 0 b6 = 0 b7 = 1 b8 = 0 ⇓ 010001011

Fig. 2-10. Computing the binary code for the integer 139.

212

2. COMPUTATION IN MATHEMATICS

makebin(n) := block([x, b, A], x : n, A : [ ], /* iteratively extract bits from right to left */ while x > 0 do (b : mod(x, 2), A : cons(b, A), x : (x - b)/2), return(A)) $ You may have noticed a certain similarity between the binary coding process described above and our logarithmic-time exponentiation function fastexp() from Section 2.1. Both functions test the parity of a given number n and, depending on the result, perform some specific task as indicated below: parity of n even odd

result for fastexp() square multiply and square

result for makebin() record a zero bit record a one bit

Then exactly the same process is repeated for the number n/2 if n is even, or (n − 1)/2 if n is odd. Evidently, although these functions have different aims, they both rely on the same deep and mysterious structure undergirding the set of integers. We can convert a binary code back to its corresponding integer by reversing the process described above. In particular, suppose that we start with the binary code bk bk−1 bk−2 . . . b2 b1 b0 . Then the decoding process begins with a value of x0 = 0 and works its way up to the integer xk+1 = n corresponding to this code by iterating the following steps: x read the value of the next bit bk−i from the binary code; y multiply xi times two; z add the value of bk−i and set xi+1 = 2 xi + bk−i .

2.7. BINARY CODES

0101011010 ⇓

213

x0 = 0 ↓

b9 = 0

−−−→

x1 = 2 · 0 + 0 = 0 ↓

b8 = 1

−−−→

x2 = 2 · 0 + 1 = 1 ↓

b7 = 0

−−−→

x3 = 2 · 1 + 0 = 2 ↓

b6 = 1

−−−→

x4 = 2 · 2 + 1 = 5 ↓

b5 = 0

−−−→

x5 = 2 · 5 + 0 = 10 ↓

b4 = 1

−−−→

x6 = 2 · 10 + 1 = 21 ↓

b3 = 1

−−−→

x7 = 2 · 21 + 1 = 43 ↓

b2 = 0

−−−→

x8 = 2 · 43 + 0 = 86 ↓

b1 = 1

−−−→

x9 = 2 · 86 + 1 = 173 ↓

b0 = 0

−−−→

x10 = 2 · 173 + 0 = 346 ⇓ n = 346

Fig. 2-11. Decoding the binary sequence 0101011010.

214

2. COMPUTATION IN MATHEMATICS

Aside from the somewhat awkward indices (which just indicate that the bits from the binary code are read from left to right) you will recognize these steps as the opposite of those taken by our encoding process, but performed in the reverse order. Fig. 2-11 illustrates this decoding process at work on the binary code 0101011010. To implement the decoding process in general, we begin with a list A of zeroes and ones, together with three local variables: ◦ i is a counter indexing each bit in A, ◦ k holds the length of the list A, and ◦ x is an accumulator that starts at zero and builds up in value according to the iterative rule above. As the index i counts up from 1 to k, it will successively point to each bit in the binary code from left to right. In the meanwhile, as this pointer advances from one bit to the next, the value of x will be doubled before the new bit A[i] is added to it and the result is once again stored in x. This process continues until the pointer has run the entire length of the binary code. The function should then return the value stored in x as its final answer. The definition below puts all of these steps together: makeint(A) := block([i, k, x], k : length(A), x : 0, /* iteratively read bits from left to right */ for i : 1 thru k do x : 2*x + A[i], return(x)) $ The third piece in our working model illustrates how a computer implements arithmetic operations (in this case, addition of integers) by performing a bit-by-bit manipulation of the corresponding binary codes. Addition in binary looks a lot like the familiar computation that we all learned in grammar school, except that instead of using the digits from zero to nine, here we are limited to just zeroes and ones. For example, adding the binary codes corresponding to 139 and 346 looks like this:

2.7. BINARY CODES 1

1

0

0

215

1

1



139

+

0 1 0 1 0 1 1 0 1 0



346

=

0 1 1 1 1 0 0 1 0 1



485

0

0

1

0

1

0

1

We begin this operation on the right hand side, adding the bits 1 + 0. The result is a sum bit of 1, which we record as the rightmost bit of our answer. The process then moves over to the next pair of bits to the left, in this case 1 + 1. The outcome here is 2, which in binary corresponds to the 2-bit code 10. Thus, we end up with a new sum bit of 0, which we record as the second bit in our answer, together with a carry bit of 1. We place this carry bit on top of the next pair of bits, and proceed to add the three bits together. This gives us a new sum bit of 1 + 0 + 0 = 1 and a new carry bit of 0. As before, we move to the left and repeat the process with the next pair of bits, continuing until we reach the two zeroes at the left. At each stage, the next set of sum and carry bits are computed according to Table 2-4. We can implement the results of this table as Maxima functions, relying on the logical operators and and or to construct the appropriate compound predicates. For instance, note that the sum bit is one precisely when there is an odd number of input bits that are equal to one. Otherwise, the sum bit is zero. This behavior is captured by the following function: sumbit(a, b, carry) := block( /* sum = 1 when an odd number of inputs are 1 */ if (a = 1 and b = 0 and carry = 0) or (a = 0 and b = 1 and carry = 0) or (a = 0 and b = 0 and carry = 1) or (a = 1 and b = 1 and carry = 1) then return(1) else return(0)) $ Similarly, the carry bit is one when at least two of the input bits are equal to one. Otherwise, the carry bit will be zero. Therefore, the

216

2. COMPUTATION IN MATHEMATICS

Table 2-4. Sum and carry bits in binary addition. bit a bit b

0 0

0 0

0 1

0 1

1 0

1 0

1 1

1 1

carry bit

0

1

0

1

0

1

0

1

sum bit

0

1

1

0

1

0

0

1

carry bit

0

0

0

1

0

1

1

1

following function computes the value of the carry bit: carrybit(a, b, carry) := block( /* carry = 1 when two or more inputs are 1 */ if (a = 1 and b = 1) or (a = 1 and carry = 1) or (b = 1 and carry = 1) then return(1) else return(0)) $ Before moving on, it is worth noting that we could have determined the final outcome of the two functions above by examining the numerical value of the sum a + b + carry. However, we chose an approach which involves logical (and not arithmetic) operations to reflect the logical underpinnings of computerized arithmetic, as well as to emphasize the fact that this is precisely what a computer’s logical circuits are doing. The addition algorithm consists of iterating the bit-by-bit addition described by the two functions above. We will describe this process in detail below, but you should try not to be daunted by the number of variables involved. We start with two lists A and B containing the binary codes of the integers that we want to add. We will also make use of several local variables:

2.7. BINARY CODES

217

◦ i is a counter pointing (from right to left) to a pair of bits in A and B, ◦ m holds the length of the list A, ◦ n holds the length of the list B, ◦ k holds the larger of these two lengths, ◦ a holds the ith bit from the right in A, ◦ b holds the ith bit from the right in B, ◦ carry holds the carry bit in the most recent bit-by-bit addition, and ◦ C contains the binary sequence of sum bits in the bit-by-bit addition. You may have noticed by this description that we will not assume that our two binary codes have the same length. In particular, this means that the counter i will range from one all the way to the larger of the two lengths, stored in the variable k as noted above. For this purpose, we shall use the primitive function max(). When i is greater than the length of one of the lists, the corresponding bit will be a leading zero. Otherwise, the bit in question is accessed by using the indexing operator on the appropriate list. In this case, the ith bit is either A[m+1-i] or B[n+1-i], since the bits must be accessed from right to left. Once we have the values of a and b in hand, we will use the two functions above to determine the next sum and carry bits. The first of these is concatenated onto the left side of the list C while the second is assigned to the variable carry. Then we proceed to the next pair of bits, repeating the same steps as above. Finally, when we reach the left end of our binary codes, we might have a leftover carry bit of 1; in such a case, this will also be concatenated onto the left side of C. The complete process for binary addition is described by the following definition: addbin(A, B) := block([i, m, n, k, a, b, carry, C], m : length(A), n : length(B), k : max(m, n), carry : 0, C : [ ],

218

2. COMPUTATION IN MATHEMATICS

/* look at bits from right to left */ for i : 1 thru k do ( /* determine next bit in A */ if i > m then a:0 else a : A[m+1-i], /* determine next bit in B */ if i > n then b:0 else b : B[n+1-i], /* compute sum and carry bits */ C : cons(sumbit(a, b, carry), C), carry : carrybit(a, b, carry)), /* after loop ends, check for leftover carry bit */ if carry = 1 then C : cons(carry, C), return(C)) $ In the exercises at the end of this section, you will have an opportunity to extend the arithmetic capabilities of our working model to subtraction and multiplication of binary sequences. Minimal as our model undoubtedly is at the present, it hopefully sheds some light on our earlier comments regarding how Maxima might evaluate an expression like 139 + 346; 485

2.7. BINARY CODES

219

At least in principle, what is actually happening in our computer’s circuits during this calculation is that each of the numbers 139 and 346 is represented by a binary sequence: A : makebin(139); [1, 0, 0, 0, 1, 0, 1, 1] B : makebin(346); [1, 0, 1, 0, 1, 1, 0, 1, 0] that these sequences are then combined to produce a third sequence: C : addbin(A, B); [1, 1, 1, 1, 0, 0, 1, 0, 1] and that this third sequence represents the final result of the addition: makeint(C); 485 As we have already noted, Maxima can perform arithmetic operations on other, more complicated, types of numbers than integers. For instance, there are fractions and other rational expressions, radical expressions involving square roots of integers, exact trigonometric and logarithmic expressions, and as we will see in the next section, floating-point numbers. All of these are represented by Maxima as binary codes of one form or another. Furthermore, any computation on these objects boils down to a manipulation of the bits in these codes. Thankfully, the actual details of the coding only become relevant when we try to push beyond the boundaries of normal computation. For example, one consequence of the standards of modern computer architecture is that the transmission of data between the various components of a computer (like the processor and the memory) is typically limited to a single 32- or 64-bit binary code at a time. For this reason, many programming languages restrict the size of an integer to either 32 or 64 bits. Exceeding this limit will result in an overflow error . However, this is not the case with Maxima. The level of precision to which Maxima can represent integers is essentially limited only by the size of a computer’s memory. Nevertheless, if you tried to evaluate the expression 2^3^4^5;

220

2. COMPUTATION IN MATHEMATICS

then you would get a somewhat severe and cryptic error message, basically telling you that Maxima does not have the storage capacity necessary to hold this number in memory and that an overflow error has occurred. After a moment of reflection, you should be able to see why. The number in question is a very large power of two that corresponds to a long binary sequence consisting of a one bit followed by a terrific number of zero bits. Maxima can compute the number of zero bits as an integer: 3^4^5; 373391848741020043532959754 . . . 196276811060702333710356481 or as a big floating-point number: bfloat(3^4^5); 3.7339184874102b488 Here, the “b488” indicates that the result, in scientific notation, has an order of magnitude of 10488 . Given that all the computers on the planet combined together would only account for about 1022 bits of memory, and that there are only about 1080 atoms in the entire universe, it is impossible to imagine a computer powerful enough to handle this kind of computation.

Exercises for Section 2.7 1.

(a) Implement a “binary step” function which takes a binary code as input and performs the following procedure: Start with rightmost bit of binary code. If it is a zero bit, then change it to a one and return the result. Otherwise, if it is one bit, change it to a zero and move to next bit on the left, repeatedly changing ones to zeroes until the first zero (possibly a leading zero) is found. Then change this zero to a one and return the result. (b) Repeat the step function above multiple times to build a binary counter that starts at zero and counts up to a given n. Be sure to print the resulting binary code after each step of the way. (c) For a silly party trick, reproduce the results from your binary counter in your hand, curling each finger down to represent a zero and extending it out to represent a one. Can you train your fingers to count from 0 to 31 without making a mistake? Can you do it without using your other hand to help?

2.

(a) Find a formula for the smallest and largest integers that, according to the working model described in this section, can be represented using n bits. (b) It is typical to set aside the leftmost bit in a binary code as a sign bit, indicating whether the integer being encoded is positive (sign bit equal to 0) or negative (sign bit equal to 1). Assuming that the remaining n − 1 bits encode the absolute value of an integer as in our working model, what are the smallest and largest integers that can be represented using n bits? According to this coding scheme, is there any integer that can be represented by more than one binary sequence? (c) What is the range of integers that can be represented by our working model using 32 bits? How about 64 bits? What if we set one sign bit aside, as in part (b)?

3. Design a function comparebin() that takes two binary codes and tests whether the first code corresponds to a greater integer than the second code. For example, your function should produce the following output: comparebin([1, 0, 1], [0, 1, 0]); true

222

2. COMPUTATION IN MATHEMATICS

comparebin([1, 0, 1], [1, 1, 0]); false comparebin([1, 0, 1], [1, 0, 0]); true comparebin([1, 0, 1], [1, 0, 1]); false Be sure to design your function so it determines its final result directly from the binary codes themselves, which might be of different lengths, and does not rely on first converting the codes to their corresponding integers. 4. Binary subtraction can be implemented in much the same way as binary addition, except that the sum bit is replaced by a difference bit and the carry bit is replaced by a borrow bit. The values of these bits are described in the table below. bit a

0

0

0

0

1

1

1

1

bit b

0

0

1

1

0

0

1

1

borrow bit

0

1

0

1

0

1

0

1

difference bit

0

1

1

0

1

0

0

1

borrow bit

0

1

1

1

0

0

0

1

(a) Define a pair of functions that compute the values of the difference and borrow bits. (b) Design a function subtractbin() that implements the algorithm for binary subtraction. (c) What happens when you attempt to subtract a larger integer from a smaller one using subtractbin()? Do you detect a pattern in such answers? 5. Binary multiplication consists of iterated shifting and adding. For example, in order to multiply 139 × 346, Maxima would perform the binary computation below: ×

+

0

1

0

1

0 0

1 1

0 0 1

=

0

1

0

1

1

1

0

0 0

0 1

1 0

0 1

0 0

0 1

1 1

0 0

1 1

1 0

0 1 1 0

1 0 0 1

0 1 1 0

1 0 1

0 1 0

1 1 1

1 0 0

0 1

1 0

0

1

1

1

1

0

1

1

1

1

0

← ←

139 346

→ 48094

EXERCISES

223

In general, we can describe the algorithm for binary multiplication as follows: We are given two binary codes A and B, and we wish to produce a new binary code representing their product. We begin by letting C be the empty binary code corresponding to the integer zero. Then, we look at the each of the bits in A from right to left and iterate through the following steps: x If the current bit in A is a one, then add B + C and store the result in C. If the current bit in A is a zero, then leave C alone. y Add an extra zero bit on the right of B, shifting all of the other bits one place to the left. z Repeat the process using the next bit in A. When we are finished with the leftmost bit in A, we will find that C contains the binary code corresponding to the product. (a) Design a function multbin() that implements the multiplication algorithm described above. (b) Perform the computation in the example above using your function and verify that the final answer corresponds to the correct integer.

2.8. Floating-Point Numbers As we noted at the end of Chapter 1, the usual practice among scientists is to use floating-point numbers to represent inexact quantities or approximate measurements. The reason for this is that a calculation involving exact expressions is typically slower and more computationally intensive than the corresponding calculation that relies exclusively on floating-point numbers. In particular, there is no reason to pay the higher computational costs when an exact result is neither required nor meaningful. As we have already seen, we can transform the result of an exact computation like sqrt(2); √ 2 into a floating-point number by using the special function float(): float(sqrt(2)); 1.414213562373095 Alternatively, we can instruct Maxima to perform the entire computation using floating-point numbers in the first place: sqrt(2.0); 1.414213562373095 These results are typically displayed using 16 digits of precision, but as we shall see there is actually much more going on behind the scenes. Instead of implementing floating-point numbers as the sixteen-digit decimals that we see on our computer screen, Maxima internally follows the IEEE-754 standard for double precision floating-point arithmetic. This means that every floating-point number is encoded using a 64-bit binary code, as indicated schematically in Fig. 2-12. The first 11 bits of this code are used to represent an integer x called the exponent, while the remaining 53 bits represent a number y known as the significand or mantissa. The pair [x | y] then corresponds to a floating-point number of the form [x | y] ↔ z = y × 2x . Note that this formulation is very similar to scientific notation, except that the order of magnitude here is expressed as a power of two rather than as a power of ten.

226

2. COMPUTATION IN MATHEMATICS

exponent x

significand y

11 bits

53 bits 64 bit word

Fig. 2-12. The IEEE-754 standard for doubleprecision floating-point numbers. The exponent is encoded by essentially the same scheme described in Section 2.7. The only difference is that the resulting integer is biased by subtracting by 1023 to produce the value of the exponent x. For instance, the binary code 00000000000 corresponds to x = −1023, while 11111111111 corresponds to x = 1024: sum(2^k, k, 0, 10) - 1023; 1024 Therefore, the exponent can take on any integer value in the interval − 1023 ≤ x ≤ 1024. However, both of the extreme values in this interval are reserved by convention for special situations. For instance, an exponent of x = 1024 is used to flag non-numerical results such as infinity, division by zero, or the square root of a negative number. On the other hand, the first bit corresponding to the significand y is a sign bit that determines whether y is positive (when the sign bit is zero) or negative (when the sign bit is one). The other 52 bits encode a binary fraction. As with our previous encounter with binary codes, the idea here is to express the significand as a sum of distinct powers of two. However, in this case, x we will consider real numbers and not just integers, y we will consider negative as well as positive powers of two, and z our sum might be infinite. To see what we mean, let us consider three rational numbers: 13 8 , and 53 . We can express the first two of these by the terminating decimals 1.625 and 1.6, while the third requires the infinitely repeating decimal 1.666 666 . . . . In a similar way, these rational numbers correspond to the following binary fractions: 8 5,

2.8. FLOATING-POINT NUMBERS 13 8



1.101,

8 5



1.100110011001100 . . . ,

5 3



1.101010101010101 . . . .

227

In particular, the first binary fraction corresponds to the finite sum 20 + 2−1 + 2−3 = 1 +

1 2

+

1 8

=

13 8 .

In contrast, the second binary fraction corresponds to an infinite sum whose terms can be grouped in pairs and rewritten as a geometric series as follows: (20 + 2−1 ) + (2−4 + 2−5 ) + (2−8 + 2−9 ) + · · · + 235 + 239 + 2313 + · · ·   1 = 32 1 + 16 + 1612 + 1613 + · · · =

3 2

=

3 2

·

16 15

= 85 .

Finally, the third binary fraction represents a sum in which all but the first term can be grouped together as a geometric series:   20 + 2−1 + 2−3 + 2−5 + 2−7 + 2−9 + · · ·   = 1 + 12 + 213 + 215 + 217 + · · ·   = 1 + 12 1 + 14 + 412 + 413 + · · · =1+

1 2

·

4 3

= 53 .

Evidently, we can produce exact finite binary representations for some numbers like 13 8 (whose denominator is a power of two), but we will require infinitely many bits to produce exact binary representations for other numbers like 85 or 53 , regardless of whether they could be written as a finite decimal. To make our discussion a little more concrete, let us now design a function that mimics the IEEE-754 representation for floating-point numbers, taking as input a positive number z and returning its exponent and the first n + 1 bits in its significand. This result is similar to that of our earlier makebin() function, except that here our function must extract the bits from left to right. Suppose, then, that we can

228

2. COMPUTATION IN MATHEMATICS

write our input as a (possible infinite) sum of powers of two starting with p0 = 2k : z=

∞ 

2k−i bi = 2k b0 + 2k−1 b1 + 2k−2 b2 + 2k−3 b3 + 2k−4 b4 + · · · .

i=0

As before each bit bi is either a zero or a one. Now, if the first bit b0 were a one, then z would consist of p0 plus some number of smaller powers of two, making it at least as large as p0 . Therefore, we can compute the first bit of our significand as  0 if z < p0 , b0 = 1 if z ≥ p0 . Similarly, we determine the next bit in the significand by comparing p1 = 2k−1 with z1 = z − p0 b0 = 2k−1 b1 + 2k−2 b2 + 2k−3 b3 + 2k−4 b4 + 2k−5 b5 + · · · , in which case, by the same reasoning as above, we get  0 if z1 < p1 , b1 = 1 if z1 ≥ p1 . The process continues iteratively with a comparison of pi = 2k−i and zi = zi−1 − pi−1 bi−1 = 2k−i bi + 2k−i−1 bi+1 + 2k−i−2 bi+2 + · · · , which again gives the next bit in the sequence:  0 if zi < pi , bi = 1 if zi ≥ pi . Our implementation of this “top-down” binary encoder starts with our inputs z and n together with four local variables: ◦ ◦ ◦ ◦

i is a counter that starts at zero and goes up to n, p is a sufficiently large power of two, k is the exponent in p, and A is a list containing our variant of the IEEE-754 encoding: an exponent (in brackets) followed by the binary fraction representing the significand.

2.8. FLOATING-POINT NUMBERS

229

Of course, before we can get the process started, we need initial values for p and k. Our implementation will begin with the convenient choice of exponent k = floor(log(z)/log(2)), where the function floor() rounds the logarithm in question down to the next lowest integer. Then, starting with p = 2k , we proceed to extract each bit of the significand by comparing z = zi and p = pi as above. When a one bit is extracted, we will subtract p from z to obtain the next value of zi+1 ; when a zero bit is extracted, no change in z is required. In either case, we divide k by 2 to get to the next value of ki+1 and repeat the comparison once again. After extracting n + 1 bits, the process stops and returns both the exponent and the binary fraction, which we have carefully stored in A. Our function definition is as follows: makefpn(z, n) := block([i, p, k, A], k : floor(log(z)/log(2)), p : 2^k, A : [[k]], /* extract binary bits from left to right by comparing z and p */ for i : 0 thru n do (if z < p then A : endcons(0, A) else (A : endcons(1, A), z : z - p), p : p / 2), return(A)) $ With this definition in place, we can compute the binary fractions given above: makefpn(13/8, 15); [[0], 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

230

2. COMPUTATION IN MATHEMATICS

makefpn(8/5, 15); [[0], 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0] makefpn(5/3, 15); [[0], 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1] Notice that all of these results produce an exponent value of zero. Other inputs will lead to different exponents, depending on their size: makefpn(1234.56789, 15); [[10], 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0] makefpn(1e-4, 15); [[−14], 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1] In all of these examples, the first (leftmost) bit in the binary fraction represents the power of two indicated by the exponent in brackets (that is to say 20 , 210 , or 2−14 ). Our choice of exponent guarantees that this first bit is always a one, so after its corresponding power of two is factored out, the significand will be given by a binary fraction of the form y = 1.b1 b2 b3 b4 b5 b6 . . . , where the first bit represents a value of 20 = 1. When the significand is put in this form, we say that it has been normalized . We can recover our original input by adding up the values of all of the ones in the normalized significand and multiplying this (possibly infinite) sum times the order of magnitude given by the exponent. For instance, if we just considered the first six ones in the last two results, we would end up the following approximations: float(1 + 1/8 + 1/16 + 1/64 + 1/512 + 1/2048) * 2^10; 1234.5 float(1 + 1/2 + 1/8 + 1/128 + 1/256 + 1/1024) * 2^-14; 9.995698928833008 10−5 The IEEE-754 standard requires that the significand be normalized whenever possible. Since this means that the first bit is a one, there is no need to encode it, so the 52 bits of space at our disposal are used to store b1 , b2 , and so on up to b52 . Together with an exponent of x,

2.8. FLOATING-POINT NUMBERS

231

these 52 bits represent the floating-point number   52  bk [x | 1.b1 b2 b3 . . . b50 b51 b52 ] ↔ 1 + × 2x . 2k k=1

As we noted above, the exponent is restricted to the interval −1022 ≤ x ≤ 1023. Therefore the largest floating-point number allowed by the standard is given by an exponent of x = 1023 (encoded by ten ones and a zero) and a significand consisting of 52 ones, corresponding to   52  1 [1023 | 1.111 . . . 111] ↔ 1 + × 21023 : 2k k=1

float((1 + sum(1/2^k, k, 1, 52)) * 2^1023); 1.797693134862316 10308 Any number larger than this will result in an overflow error , usually accompanied by a moderately belligerent error message telling you that the number in question is “too large to be represented as a double-float.” On some occasions, however, the IEEE-754 standard does allow the first bit in a binary fraction to be a zero, representing a denormalized significand of the form y = 0.b1 b2 b3 b4 b5 b6 . . . . This is particularly important when we want to represent a significand of zero, since it corresponds to a binary fraction consisting of all zero bits and, consequently, can never be normalized. The extreme exponent value of x = −1023 is used to flag a denormalized significand, in which case the remaining 52 bits of the binary code are used to represent the floating-point number [−1023 | 0.b1 b2 b3 . . . b50 b51 b52 ] ↔

52  bk × 2−1022 . 2k

k=1

For example, the smallest positive floating-point number in Maxima is represented by an exponent of x = −1023 (encoded by eleven zeroes) and a denormalized significand consisting of 51 zero bits followed by a single one bit, and is therefore equal to [−1023 | 0.000 . . . 001] ↔

1 × 2−1022 : 252

232

2. COMPUTATION IN MATHEMATICS

float(1/2^52 * 2^-1022); 4.940656458412465 10−324 Entering a smaller positive number produces an underflow error , which typically results in rounding the number in question down to zero. Note, however, that some computer platforms will avoid underflow rounding and instead convert the offending result into a different binary coding scheme. Still, other platforms forbid the use of nonzero denormalized significands altogether, so even larger numbers (on the order of 10−308 ) can result in underflow errors. In either case, even though an underflow error has occurred, Maxima does nothing to warn us about it. Evidently, only a small proportion of real numbers between these two extremes can be represented exactly by the IEEE-754 scheme; all others must be rounded to the nearest floating-point number that can actually be represented by the standard. Such rounding is considered benign since it is an inevitable consequence of the finite precision inherent in any implementation. In general, the nearest floating-point number below any given real number is found by truncating the significand after the 52nd bit. We call this the round-down value for that number. On the other hand, the nearest floating-point number above a given number is found by adding a one to the 52nd bit of the round-down value and performing all of the ensuing carries. This gives us its round-up value. The decision as to whether a number should be rounded up or rounded down depends on which of these two values is closer, and this in turn depends on the bits that are dropped by the truncation. In particular, Maxima looks at the bits in the 53rd , 54th , and 55th slots to decide whether to round up or down. In the simplest of cases, a 53rd bit of zero indicates that a number is closer to its round-down value, and should therefore be rounded down. We can see an example of this type of rounding in the binary representation for z = 255 + 43, which we compute using 55 bits. Observe how the last three bits are replaced by zeroes when the input is rounded to its floating-point equivalent by float(): makefpn(2^55 + 43, 55); [[55], 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, · · · , 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1]

2.8. FLOATING-POINT NUMBERS

233

makefpn(float(2^55 + 43), 55); [[55], 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, · · · , 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0] In contrast, a number is closer to its round-up value when it has a 53rd bit of one, followed by another one in the 54th or 55th slot. In this case, the number in question should be rounded up. This is the situation for z = 255 + 45; in particular, you will note that both the 51st and 52nd bits are modified as a result of the rounding: makefpn(2^55 + 45, 55); [[55], 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, · · · , 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1] makefpn(float(2^55 + 45), 55); [[55], 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, · · · , 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0] Finally, if a number has a 53rd bit of one followed by only zeroes, then it lies exactly halfway between its round-up and round-down values. In this case, we adopt the convention that a number is rounded down when its 52nd bit is zero and rounded up when its 52nd bit is one. This “tie-breaking” strategy guarantees that we round down half of the time and round up the other half. For example, z = 255 + 52 rounds down: makefpn(2^55 + 52, 55); [[55], 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, · · · , 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0] makefpn(float(2^55 + 52), 55); [[55], 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, · · · , 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0] while z = 255 + 44 rounds up: makefpn(2^55 + 44, 55); [[55], 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, · · · , 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0] makefpn(float(2^55 + 44), 55); [[55], 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, · · · , 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0] As these examples illustrate, when the tie-breaking strategy is invoked, the rounded binary fraction always ends with a 52nd bit of

234

2. COMPUTATION IN MATHEMATICS

zero. This strategy is commonly known as unbiased rounding or rounding to even.6 Now that we have a better understanding of how Maxima actually implements floating-point numbers, we are finally in a position to address the unusual rounding behavior observed at the end of Section 1.7. For example, we noted an incorrect answer when multiplying 12.3 * 56.7; 697.4100000000001 This problem can be explained as an accumulation of benign roundoff error at the binary level. In particular, both 12.3 and 56.7 round up when converted to binary: makefpn(123/10, 55); [[3], 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, · · · , 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0] makefpn(12.3, 55); [[3], 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, · · · , 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0] makefpn(567/10, 55); [[5], 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, · · · , 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0] makefpn(56.7, 55); [[5], 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, · · · , 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0] These very small roundoff errors persist after the two numbers are multiplied together, producing a larger answer than was expected. In effect, the accumulation of error amounts to rounding the product up, when it ought to have been rounded down: makefpn(123/10 * 567/10, 55); [[9], 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, · · · , 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0] makefpn(12.3 * 56.7, 55); [[9], 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, · · · , 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0]

6 Banking institutions use a similar method to round decimals ending in a five. Numbers for which the next-to-last digit is odd are rounded up, and those for which it is even are rounded down. Hence, the result always ends in an even digit.

2.8. FLOATING-POINT NUMBERS

235

Because of this tiny discrepancy, Maxima incorrectly interprets the result as the slightly larger value of 697.41 + 1.0 × 10−13 : makefpn(697.41 + 1e-13, 55); [[9], 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, · · · , 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0] An accumulation of benign roundoff error also lurks behind the incorrect answer to 12.34 * 567.8; 7006.651999999999 In this case, when 12.34 and 567.8 are converted into binary, they are rounded down: makefpn(1234/100, 55); [[3], 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, · · · , 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0] makefpn(12.34, 55); [[3], 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, · · · , 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0] makefpn(5678/10, 55); [[9], 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, · · · , 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1] makefpn(567.8, 55); [[9], 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, · · · , 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0] After the multiplication, these roundoff errors give a smaller answer than expected, essentially rounding down a product that should have rounded up: makefpn(1234/100 * 5678/10, 55); [[12], 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, · · · , 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1] makefpn(12.34 * 567.8, 55); [[12], 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, · · · , 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0] Again, the small roundoff error causes Maxima to confuse the result with the smaller, incorrect value of 7006.652 − 1 × 10−12 : makefpn(7006.652 - 1e-12, 55); [[12], 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, · · · , 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0]

236

2. COMPUTATION IN MATHEMATICS

The third roundoff error we observed came from a computation that clearly should have resulted in an answer of 2 × 10−8 : 123456789.12345678 - 123456789.12345676; 1.490116119384766 10−8 This mysterious result is a case of loss of significance nearly resulting in a catastrophic roundoff error, but again at the level of binary. In particular, note that the binary representation of the two numbers involved agree in all but the last three bits: makefpn(123456789.12345678, 52); [[26], 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, · · · , 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0] makefpn(123456789.12345676, 52); [[26], 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, · · · , 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1] The subtraction will cancel all of the bits held in common, leaving behind a single one bit in the 52nd slot. Given the exponent of 26, this bit represents a value of float(1/2^52 * 2^26); 1.490116119384766 10−8 Notice that this value is precisely the answer we obtained above. Whenever a real number is translated to its floating-point representation, the roundoff error is no larger than the contribution of its 53rd bit which, depending on the size of the significand, accounts for a relative error of between 2−53 and 2−54 : float(2^-53); 1.110223024625157 10−16 float(2^-54); 5.551115123125783 10−17 Therefore the precision of a floating-point operation is comparable to using sixteen decimal digits, where the relative error due to benign rounding can range anywhere from 5 × 10−16 to 5 × 10−17 . It is sometimes a little better, but as the examples above show, it is occasionally a little worse. We can control the number of digits that Maxima displays, and thus avoid many bothersome roundoff errors, by changing the value of the global variable fpprintprec. For instance, we can

2.8. FLOATING-POINT NUMBERS

237

ask Maxima to display a maximum of 15 digits by entering: fpprintprec : 15; 12.3 * 56.7; 697.41 12.34 * 567.8; 7006.652 However, it should be noted that fpprintprec only controls the way that these floating-point numbers are displayed, and does not affect their binary implementation in the least. In particular, whatever roundoff errors were associated with these values will persist in our computations even though we can no longer see them. Out of sight, out of mind; but scientific programmers beware!

Exercises for Section 2.8 1. Depending on when we convert to floating-point numbers, Maxima evaluates the expression  √ 3/2 3 2 4 as either 1.092356486341477 or 1.092356486341478. For example: (3/4 * sqrt(2))^1.5; 1.092356486341477 And also: (3/4 * sqrt(2.0))^(3/2); 1.092356486341478 Which of these decimals is more precise? Which is more accurate? Explain. 2. When prompted to evaluate N = 2^3^4^5 either as an integer or as a floating-point number, Maxima will return an overflow error. (a) Use a logarithm base 10 to determine how many decimal digits would be needed to write N down in its entirety. Express your answer as a big floating-point number. (b) Use a logarithm base 2 to determine how many binary bits would be needed to represent N using the binary coding scheme from Section 2.7. (c) Suppose that N could be represented as a floating-point number of the form N = y × 2x . How many bits would be needed to represent the exponent x in binary? 3. Consider the following modifications to the function makefpn() defined in this section. (a) Modify makefpn() so it always returns 53 bits of the binary fraction, removing the need for the input n. (b) Modify makefpn() so it returns a polite error message announcing an overflow error whenever the exponent is 1024 or above. (c) Modify makefpn() so it encodes a denormalized significant whenever the exponent is −1023 or below. 4. Implement a “top-down” version of makebin() that computes the binary code for any non-negative integer from left to right. Be

EXERCISES

239

sure to test that your function correctly encodes both even and odd integers. 5. Implement an iterative logarithmic-time function that computes bn for a given integer n ≥ 0 as follows: Start with an accumulator initially set to one, and consider the binary code for n from left to right. Each zero bit in this binary code corresponds to a squaring step, while every one bit corresponds to squaring and multiplying times b. The process should stop when it reaches the rightmost bit in the binary code.

CHAPTER 3

Graphics and Visualization “The mind is confused? Is it not so? Take time, mon ami. You are agitated; you are excited—but it is natural. Presently, when we are calmer, we will arrange the facts, neatly, each in his proper place. We will examine—and reject. Those of importance we will put on one side; those of no importance, pouf! ... blow them away!” — Hercule Poirot as quoted in The Mysterious Affair at Styles by Agatha Christie (1920)

3.1. Celestial Mechanics As we saw in our discussions of Leibnitz’s series and of Archimedes’ sequence, pictures can help us to arrange a collection of computational facts “neatly” and “each one in his proper place,” as it were. Indeed, in Sections 1.5 and 1.6, we used the wxdraw2d() command to plot sequences of points, to graph parametric equations and explicit functions, and to combine these graphical elements together into a single plot. It is quite reasonable, then, to follow the advise of a certain Belgian detective, to examine some of these concepts in greater detail, and to explore some of the other capabilities of Maxima’s drawing commands. These shall be our main goals in this chapter. There are four commands that will be of particular interest to us here: draw2d()

wxdraw2d()

draw3d()

wxdraw3d()

These commands are stored as part of the draw package in Maxima’s external library and are fully functional only when this package is loaded into the kernel as follows: load(draw) $ As their names indicate, these commands allow us to draw two- and three-dimensional graphs. The two wxdraw commands produce in-line plots within the wxMaxima iris that can be saved in an xml document along with the rest of the contents of your Maxima session. In contrast, the two draw commands produce external plots in a separate window. Although two-dimensional external plots do not provide any value over their in-line counterparts, three-dimensional external plots have the benefit that they can be rotated on the screen, allowing you to change your perspective and better experience them as virtual objects. All of the drawing commands supply Maxima with instructions for drawing a “scene” composed of one or more graphical objects. Each one of these objects is built by a graphic constructor together with several options specifying the way it should be displayed. We have already encountered three of these constructors, as well as several options. Table 3-1 lists some of the most useful graphic constructors available for two-dimensional graphs. In this section, we will construct a simple visual representation of the inner solar system, consisting of the four planets closest to the sun

244

3. GRAPHICS AND VISUALIZATION

Table 3-1. Two-dimensional graphic constructors.

explicit()

implicit()

label()

parametric()

points()

polar()

rectangle()

triangle()

quadrilateral()

and their orbits. Like much of our modern understanding of celestial mechanics, the mathematics required to build this scene is primarily due to the German astronomer Johann Kepler (1571 – 1630), who at the beginning of the seventeenth century proposed his three famous laws of planetary motion: x Every planet orbits around the sun with a trajectory in the shape of an ellipse having the sun at one of its foci. y As each planet orbits the sun, the line between the planet and the sun sweeps out equal area in equal time. z The square of the time it takes a planet to complete one revolution around the sun is proportional to the cube of its average distance to the sun. Kepler’s first law is the key to completing our graphical representation. In particular, it implies that the distance between a planet and the sun varies throughout the orbit, as can be seen in Fig. 3-1. Note that the point on a planet’s orbit where it comes closest to the sun (on the left side of the figure) is known as its periphelion, while the point where it is farthest away from the sun (on the right side of the figure) is known as its aphelion. As we shall see, these two positions completely determine the size and shape of an elliptical orbit. For example, the semi-major axis (measuring half the length of the ellipse), the semi-minor axis (measuring half the height the ellipse), and the focal displacement (the distance from the center of the ellipse to one of the foci) can all be expressed in terms of the aphelion and perihelion. Table 3-2 gives us the data needed to produce our heliocentric graphic in terms of two curiously geocentric units of measure: the radius of the earth and its average distance to the sun. (The latter is known as an astronomical unit.) Some of our extraterrestrial

3.1. CELESTIAL MECHANICS

245

Table 3-2. Size of the planets in the solar system and their orbits around the sun. Planetary radii are given in terms of the radius of the earth (6 371 km). Aphelia and perihelia are given in astronomical units (1 au = 1.496 × 108 km). Planet

Radius

Aphelion

Perihelion

Mercury

0.383

0.467

0.307

Venus

0.949

0.728

0.718

Earth

1.000

1.017

0.983

Mars

0.532

1.666

1.381

Jupiter

11.2

5.459

4.950

Saturn

9.45

10.12

9.041

Uranus

4.01

20.08

Neptune

3.88

30.39

18.32 29.71

Source: nssdc.gsfc.nasa.gov/planetary/factsheet (July 2016)

periphelion

aphelion

planet

sun

semi-minor axis

focal displacement

semi-major axis

Fig. 3-1. Anatomy of an elliptical orbit.

246

3. GRAPHICS AND VISUALIZATION

Table 3-3. Point types recognized by the draw package. · +

dot plus

+ × ×

asterisk multiply



square circle



filled square filled circle



diamant



 filled up triangle  filled down triangle 



 up triangle  down triangle

filled diamant

neighbors might consider this choice a bit self-centered, but we can disregard their concerns, at least for the time being. To start our graphic, we shall draw each of the planets, along with the sun, using the graphic constructor points(). We will specify their shape, color, and size with the following options: ◦ point_type = filled_circle will plot each point as a circular disk with its interior filled in; other possible choices for point type are given in Table 3-3. ◦ color will draw each point in a different color (yellow for the sun, dark_gray for Mercury, dark_khaki for Venus, royalblue for earth, and orange_red for Mars); other possible color choices are given in Table 3-4. ◦ point_size will render each point in a different size, proportional to the radius of the corresponding planet. We will draw the earth using a point size of 3, so the sizes for the other three planets can be determined as follows: 3 * 0.383; 3 * 0.949; 3 * 0.532; 1.149 2.847 1.596 Since the radius of sun is roughly 109 times radius of earth, we will

Table 3-4. Colors recognized by the draw package.

black gray10 gray20 gray30 gray40 gray50 gray60 dark_gray gray70 gray gray80 light_gray gray90 white pink light_pink dark_pink red light_red dark_red brown dark_orange orange_red coral light_coral dark_salmon salmon light_salmon orange goldenrod gold yellow dark_goldenrod

dark_khaki dark_yellow light_goldenrod khaki beige light_green green forest_green dark_green sea_green spring_green dark_turquoise turquoise aquamarine dark_cyan cyan light_turquoise light_cyan light_blue skyblue royalblue blue medium_blue dark_blue navy midnight_blue dark_violet dark_magenta purple magenta light_magenta violet plum

3.1. CELESTIAL MECHANICS

247

have to settle for drawing it larger than the four planets (even if not proportionally so) using a point size of 10. We can then render the initial phase of our graphic by placing the sun at the origin and the planets along the positive x-axis according to their aphelia: wxdraw2d(point_type = filled_circle, color = yellow, point_size = 10, points([[0,0]]), color = dark_gray, point_size = 1.15, points([[0.467,0]]), color = dark_khaki, point_size = 2.85, points([[0.728,0]]), color = royalblue, point_size = 3, points([[1.017,0]]), color = orange_red, point_size = 1.6, points([[1.666,0]]));

Although visually pleasing, we should keep in mind that this graphic is not drawn to scale. Not only is the sun much larger than we have drawn it, but the planets themselves are much farther apart from one another. Indeed, the radius of the sun is only about 4.649 × 10−3 astronomical units, while that of the earth is a meager 4.263 × 10−5 astronomical units. This means that if we placed the sun and Mars on opposite margins of this page, the image of the sun would only measure a couple of hundredths of an inch in diameter, and the four planets would be completely invisible.

248

3. GRAPHICS AND VISUALIZATION

Our next step will be to add the orbits of our four planets, which will construct in four different ways. Since each of these will require the same set of graphic options, we first call on the function set_draw_defaults() to redefine our defaults as follows: set_draw_defaults(xlabel = "", ylabel = "", proportional_axes = xy, grid = true, xaxis = true, yaxis = true) $ This command will make sure that all subsequent graphs are drawn according to the following specifications: ◦ xlabel = "" and ylabel = "" eliminate the labels on the bottom and left side of the graph. ◦ proportional_axes = xy forces the use of equally-scaled axes. ◦ grid = true draws a grid of horizontal and vertical lines. ◦ xaxis = true and yaxis = true draw the x- and y-axes. We can describe any ellipse with one focus at the origin and the other along the positive x-axis by an equation of the form 

x−c a

2 +

 2 y = 1. b

Here, a is the semi-major axis, b is the semi-minor axis, and c is the focal displacement of the ellipse (see Fig. 3-1). We can express each of these distances in terms of the aphelion (aph) and perihelion (peri) of the planet as follows: a : (aph + peri)/2; peri + aph 2 b : sqrt(aph * peri); √ aph peri c : (aph - peri)/2; aph − peri 2 Our equation for an elliptical orbit then becomes:

3.1. CELESTIAL MECHANICS

249

orbit : ((x-c)/a)^2 + (y/b)^2 = 1;  2 aph − peri 4 x − y2 2 + =1 2 aph peri (peri + aph) Then substituting the values from Table 3-2 corresponding to Mercury, we obtain: mercury : subst([aph = 0.467, peri = 0.307], orbit); 6.975008544385466 y 2 + 6.67694916838598 (x −0.08000000000000002)2 = 1 This equation expresses the relationship between the x- and ycoordinates of all points in Mercury’s orbit, and yet it has not been solved for either variable. We say that this is an implicit equation because it expresses an implicit relationship between x and y. We can plot this type of equation using the graphic constructor implicit(). Its syntax includes the equation to be plotted and the names of its two variables, each followed by its minimum and maximum value: wxdraw2d(implicit(mercury, x, -0.5, 0.5, y, -0.5, 0.5)); rat: replaced 6.67694916838598 by 1000000/149769 rat: replaced -0.08000000000000002 by -2/25 rat: replaced 6.975008544385466 by 1000000/143369

250

3. GRAPHICS AND VISUALIZATION

Besides a plot of Mercury’s orbit, Maxima gives some warnings about the algorithm she uses to produce the implicit graph. They indicate that the function rat() has been used to convert floating-point numbers into exact rational numbers, much like we have used the function float() to convert exact expressions into floating-point numbers. We can avoid such warnings by defining this orbit with rational expressions in the first place: mercury : subst([aph = 467/1000, peri = 307/1000], orbit) $ Then we can incorporate this orbit into our model of the inner planets as follows: wxdraw2d(point_type = filled_circle, color = yellow, point_size = 10, points([[0,0]]), color = dark_gray, point_size = 1.15, points([[0.467,0]]), implicit(mercury, x, -0.5, 0.5, y, -0.5, 0.5), color = dark_khaki, point_size = 2.85, points([[0.728,0]]), color = royalblue, point_size = 3, points([[1.017,0]]), color = orange_red, point_size = 1.6, points([[1.666,0]]));

3.1. CELESTIAL MECHANICS

251

Assuming that an implicit equation is sufficiently simple, we can solve for one of the variables and obtain one or more equations expressing the relationship between the variables explicitly. For instance, Maxima’s solve() command has no problem in doing this for a generic planetary orbit: soln : solve(orbit, y);  2 −aph peri x2 − aph peri2 x + aph2 peri x + aph2 peri2 [y = − peri + aph  2 2 −aph peri x − aph peri2 x + aph2 peri x + aph2 peri2 ] y= peri + aph This produces two solutions, each one describing half of an ellipse. We refer to an equation of this kind as an explicit equation, and plot its graph using the graphic constructor explicit(), whose syntax we saw earlier in Chapter 1. For example, after substituting the orbital data for Venus into the second solution (which in this case happens to be the positive one), we obtain an explicit equation for the top half of its elliptical orbit. venus : subst([aph = 0.728, peri = 0.718], rhs(soln[2])); 1.383125864453665 √ −0.522704 x2 + 0.005227040000000016 x + 0.273219471616 wxdraw2d(explicit(venus, x, -0.718, 0.728));

252

3. GRAPHICS AND VISUALIZATION

Similarly, the other solution is an explicit equation describing the bottom half of the orbit. By symmetry, this simply corresponds to -venus, so we can draw the whole of Venus’s orbit by invoking the explicit() constructor twice, as in the following: wxdraw2d(point_type = filled_circle, color = yellow, point_size = 10, points([[0,0]]), color = dark_gray, point_size = 1.15, points([[0.467,0]]), implicit(mercury, x, -0.5, 0.5, y, -0.5, 0.5), color = dark_khaki, point_size = 2.85, points([[0.728,0]]), explicit(venus, x, -0.718, 0.728), explicit(-venus, x, -0.718, 0.728), color = royalblue, point_size = 3, points([[1.017,0]]), color = orange_red, point_size = 1.6, points([[1.666,0]]));

Observe that the equation of an ellipse comprises two squares that add up to one. This means that it is also possible to use trigonometry to describe a planet’s motion around the sun as follows: ⎧ ⎨x(ω) = a cos(ω) + c ⎩y(ω) = b sin(ω).

3.1. CELESTIAL MECHANICS

253

Indeed, substituting these formulas into our orbital equation yields a familiar identity from trigonometry: X : a * cos(omega) + c $ Y : b * sin(omega) $ subst([x = X, y = Y], orbit); sin(ω)2 + cos(ω)2 = 1 We refer to the variable ω as a parameter , and to the pair of equations describing the coordinate functions x(ω) and y(ω) as parametric equations. Maxima plots these types of equations using the graphic constructor parametric(), which we have also seen before. Since both of the coordinate functions above repeat after a period of 2π, we can use this constructor to draw the earth’s orbit as follows: earth_x : subst([aph = 1.017, peri = 0.983], X); 1.0 cos(ω) + 0.01699999999999996 earth_y : subst([aph = 1.017, peri = 0.983], Y); 0.9998554895583661 sin(ω) wxdraw2d(parametric(earth_x, earth_y, omega, 0, 2*%pi));

It is often useful to think about a parameter as corresponding to time. In that case, a pair of parametric equations describe how a

254

3. GRAPHICS AND VISUALIZATION

particle’s position changes as it moves along its trajectory. In our particular example, the particle in question would be our tiny blue planet making its way around the sun. However, it turns out that thinking about ω in this way does not describe the real motion of an orbiting planet. In fact, the time that it takes a given planet to move around its orbit can itself be described as a function of ω as ' 1 & c t(ω) = ω + sin(ω) . 2π a In the case of planet earth, this becomes: T : (omega + c/a * sin(omega))/(2*%pi); sin (ω) (aph − peri) +ω peri + aph 2π earth_t : subst([aph = 1.017, peri = 0.983], T); 0.01699999999999996 sin (ω) + ω 2π To see the difference, let us consider how the planet’s speed varies, first when we think of ω as time and then when we correctly treat t as time. As you might recall, the planet’s speed in each of these situations is given by the formulas   dx 2 dy 2 dx 2 dy 2 dω ds ds = = , + and + · dω dω dω dt dω dω dt which we can graph as follows: wxdraw2d(proportional_axes = none, explicit(sqrt(diff(earth_x, omega)^2 + diff(earth_y, omega)^2), omega, 0, 2*%pi));

3.1. CELESTIAL MECHANICS

255

wxdraw2d(proportional_axes = none, explicit(sqrt(diff(earth_x, omega)^2 + diff(earth_y, omega)^2)/diff(earth_t, omega), omega, 0, 2*%pi));

Observe that the first of these graphs indicates that speed varies between minimum values at ω = 0, π, and 2π (when the planet is at its aphelion or its perihelion) and maximum values at ω = π2 and 3π 2 (as the planet passes its orbit’s semi-minor axis). This is in violation of Kepler’s second law, which implies that a planet moves slower when it is farther away from the sun. In contrast, the second graph correctly

256

3. GRAPHICS AND VISUALIZATION

shows that speed is greatest at the planet’s perihelion and least at its aphelion, as predicted by Kepler’s second law. Before adding the earth’s orbit to our growing model of the inner solar system, you should look closely at the parametric graph we produced above. Notice that instead of a smooth elliptical arc, our graph looks slightly polygonal. We will explore the reason for this curious behavior in the Section 3.6. For now, we can avoid the problem by increasing our graph’s resolution with the option nticks; a value of 75 will produce a nice smooth-looking orbit as follows: wxdraw2d(point_type = filled_circle, nticks = 75, color = yellow, point_size = 10, points([[0,0]]), color = dark_gray, point_size = 1.15, points([[0.467,0]]), implicit(mercury, x, -0.5, 0.5, y, -0.5, 0.5), color = dark_khaki, point_size = 2.85, points([[0.728,0]]), explicit(venus, x, -0.718, 0.728), explicit(-venus, x, -0.718, 0.728), color = royalblue, point_size = 3, points([[1.017,0]]), parametric(earth_x, earth_y, omega, 0, 2*%pi), color = orange_red, point_size = 1.6, points([[1.666,0]]));

3.1. CELESTIAL MECHANICS

257

The equation for an elliptical orbit becomes surprisingly simple when translated into polar coordinates, when it takes the form r(θ) =

b2 . a − c cos(θ)

In terms of an orbit’s aphelion and perihelion, this equation can be rewritten as: R : radcan(b^2/(a-c*cos(theta))); 2 aph peri (peri − aph) cos (θ) + peri + aph At first sight, a polar equation of this kind appears to be an explicit equation for the orbit, except that here the coordinate r is not measured horizontally (like x) or vertically (like y) but radially away from the origin in the direction of the angle θ. We can translate from polar to rectangular coordinates and back by recalling the following dictionary of formulas: x = r cos(θ)

r 2 = x2 + y 2

y = r sin(θ)

tan(θ) = y/x.

For example, we can obtain the equation above by substituting the first two of these formulas into orbit and solving for r: orbit1 : subst([y^2 = r^2 - x^2, x = r*cos(theta)], orbit);  2 aph − peri 4 r cos (θ) − 2 r2 − r2 cos (θ) 2 =1 + 2 aph peri (peri + aph) soln : solve(orbit1, r); 2 aph peri , aph (cos (θ) − 1) + peri (− cos (θ) − 1) 2 aph peri ] r=− aph (cos (θ) + 1) + peri (1 − cos (θ))

[r = −

radcan(rhs(soln[1])); 2 aph peri (peri − aph) cos (θ) + peri + aph

258

3. GRAPHICS AND VISUALIZATION

The graphic constructor polar() allows us to plot equations like the one above. The syntax is similar to that of the explicit() constructor, comprising the polar equation, the name of the variable used to represent θ, and its two bounds (typically 0 and 2π, although exceptions are possible). For example, we graph the polar equation for Mars’s orbit as follows: mars : subst([aph = 1.666, peri = 1.381], R); 4.601491999999999 3.047 − 0.2849999999999999 cos (θ) wxdraw2d(polar(mars, theta, 0, 2*%pi));

Note that this graph also exhibits a slight polygonal character, but we can remedy this with a higher value of nticks as in the following: wxdraw2d(point_type = filled_circle, nticks = 75, color = yellow, point_size = 10, points([[0,0]]), color = dark_gray, point_size = 1.15, points([[0.467,0]]), implicit(mercury, x, -0.5, 0.5, y, -0.5, 0.5), color = dark_khaki, point_size = 2.85, points([[0.728,0]]), explicit(venus, x, -0.718, 0.728), explicit(-venus, x, -0.718, 0.728), color = royalblue, point_size = 3,

3.1. CELESTIAL MECHANICS

259

points([[1.017,0]]), parametric(earth_x, earth_y, omega, 0, 2*%pi), color = orange_red, point_size = 1.6, points([[1.666,0]]), polar(mars, theta, 0, 2*%pi));

With all of the elements of our graphic in place, we can finish off our drawing by giving it a bit of style. We start by setting our drawing defaults a second time: set_draw_defaults(proportional_axes = xy, point_type = filled_circle, nticks = 75, axis_top = false, axis_bottom = false, axis_left = false, axis_right = false, xtics = false, ytics = false) $ We are already familiar with the first three of these options. The remaining options suppress the usual rectangular border around our graphs, as well as the numbered “tick marks” that usually run along the x and y-axes. We can now produce our final representation of the inner solar system with the following command: wxdraw2d(color = yellow, point_size = 10, points([[0,0]]), color = dark_gray, point_size = 1.15, points([[0.467,0]]),

260

3. GRAPHICS AND VISUALIZATION

implicit(mercury, x, -0.5, 0.5, y, -0.5, 0.5), color = dark_khaki, point_size = 2.85, points([[0.728,0]]), explicit(venus, x, -0.718, 0.728), explicit(-venus, x, -0.718, 0.728), color = royalblue, point_size = 3, points([[1.017,0]]), parametric(earth_x, earth_y, omega, 0, 2*%pi), color = orange_red, point_size = 1.6, points([[1.666,0]]), polar(mars, theta, 0, 2*%pi));

Once we are satisfied with the result, we can save it as an image file to share with all of our friends, even those poor unfortunate souls who do not have Maxima installed on their computers. We can call forth the appropriate dialogue window by right-clicking the image with our mouse and selecting the “Save Image...” option.

Exercises for Section 3.1 1. Complete our model of the solar system by adding the remaining four planets listed in Table 3-2. Note that you may need to adjust the point size used for the larger planets so that they do not run into one another. An internet search should help you select appropriate colors for the planets. 2. The following polar equation describes a curve known as a conic section: 1 r(θ) = . 1 − ε cos(θ) The constant ε, which is known as the eccentricity, determines the overall shape of the curve. Use Maxima’s drawing capabilities and an appropriate choice of ε to verify empirically each of the following statements. ◦ If ε = 0 then the curve is a circle. ◦ If 0 < ε < 1 then the curve is an ellipse. ◦ If ε = 1 then the curve is a parabola. ◦ If ε > 1 then the curve is a hyperbola. 3. The following implicit equation describes a curve known as an elliptic curve: y 2 = x3 + ax + b. The constants a and b determine the overall shape of the curve. Use Maxima’s drawing capabilities and an appropriate choice of constants to verify empirically each of the following statements. ◦ If 4a3 + 27b2 > 0 then the curve has a single connected component. ◦ If 4a3 + 27b2 < 0 then the curve has two connected components. ◦ If 4a3 + 27b2 = 0 then the curve has a self-intersection or a sharp cusp. 4. In the field of descriptive statistics, a box and whisker diagram is a way of graphically representing the five most important sample percentiles of a set of numbers: ◦ the minimum or smallest value in the set, ◦ the lower quartile value, larger than 1/4 of the elements in the set, ◦ the median, or middle value in the set,

262

3. GRAPHICS AND VISUALIZATION

◦ the upper quartile value, larger than 3/4 of the elements in the set, and ◦ the maximum or largest value in the set. For example, the box and whisker diagram for the first fifteen prime numbers is shown below:

(a) The box in the diagram stretches from the lower quartile to the upper quartile. When the elements of a set are arranged within a list X, you can compute these values by loading the stats package and evaluating the expressions quantile(X, 1/4) and quantile(X, 3/4). The box itself may be drawn using the rectangle() graphic constructor, which takes an input of two points that it uses as diagonally opposite corners of a rectangle. The color of the inside of the rectangle is controlled by the fill_color option. Compute the two quartile values for the first fifteen primes and draw the corresponding box. (b) The median is indicated in the diagram by a short vertical line inside the box. This line may be drawn by the points() graphic constructor. Compute the median for the first fifteen primes using the function median(), and then draw the corresponding line inside the box from part (a). (c) The whisker in the diagram consists of a horizontal line stretching from the minimum to the maximum value, together with a short vertical line at each end. These vertical lines should be slightly shorter than the median line. Compute the minimum and maximum values for the first fifteen primes using the functions lmin() and lmax(), and then draw the corresponding whisker on the graph from part (b). (d) Repeat the steps above to draw the box and whisker diagram for the first fifteen digits of π, and for the first fifteen digits of e. Do your diagrams show any similarities or differences with the diagram above? 5.

(a) When a three-dimensional cube is displayed on a two-dimensional screen, the image often appears as three quadrilaterals that together form a hexagon, as illustrated below. Use the quadrilateral() graphic constructor to build such an image. Note that this constructor takes an input of four points that are used as the corners of the quadrilateral. Use the

EXERCISES

263

fill_color option to shade the inside of each quadrilateral with a different color.

cube (b) An octahedron is a three-dimensional solid with eight triangular faces; its vertices are located at the centers of the faces of a cube. When an octahedron is displayed on a twodimensional screen, the image often appears as four triangles that together form a quadrilateral, as illustrated below. Use the triangle() graphic constructor to build such an image. This constructor has similar syntax to quadrilateral(), but only takes an input of three points. As before, use the fill_color option to shade the inside of each triangle with a different color.

octahedron

3.2. An Epitaph for Archimedes Archimedes died in 212 B.C. as Syracuse fell to Roman forces during the Second Punic War. According to legend he was in the midst of studying a geometric diagram when a Roman soldier burst into his home, giving the old mathematician just enough time to shout, “Noli turbare circulos meos! ” or “Do not disturb my circles!” We may never know what sorts of circles so tragically captured Archimedes’ attention moments before his death, but we do know of the diagram that adorned his tomb afterwards: a sphere inscribed within a cylinder, illustrating his favorite result that the volume of a sphere is two-thirds that of a cylinder with the same height and radius. In this section, we shall construct a three-dimensional reproduction of this mournful diagram. As we already noted, the difference between Maxima’s three-dimensional drawing commands is that draw3d() produces an external plot that can be rotated on the screen while wxdraw3d() produces a static in-line plot within the wxMaxima iris. To get a better sense and appreciation of the three-dimensional nature of your graphs, you are encouraged to always use the draw3d() command. However, for the purposes of legibility, in the following pages we shall display all images as in-line plots, as they would appear if we had used the wxdraw3d() command instead. Like their two-dimensional analogues, draw3d() and wxdraw3d() are fully functional only after the draw package is loaded into the kernel’s memory: load(draw) $ Once this is done, we have access to several graphic constructors for rendering three-dimensional plots; Table 3-5 lists the most important of these. Maxima allows us to draw four different styles of plots, as shown in Fig. 3-2, depending on the values of enhanced3d, surface_hide, and wired_surface. These options control the use of color, transparency, and texture in our graphs. In this section, we will choose the fourth of these styles by setting our default options to include enhanced3d = true and wired_surface = true. While we are at it, we will also add the following default options: ◦ palette and color allow us to specify a selection of surface and gridline colors,

266

3. GRAPHICS AND VISUALIZATION

Table 3-5. Three-dimensional graphic constructors.

cylindrical()

explicit()

implicit()

mesh()

parametric()

parametric_surface()

points()

quadrilateral()

rectangle()

spherical()

triangle()

tube()

enhanced3d = false surface hide = false

enhanced3d = false surface hide = true

enhanced3d = true wired surface = false

enhanced3d = true wired surface = true

Fig. 3-2. Four different styles of three dimensional graphs with varying choices of color, transparency, and texture.

3.2. AN EPITAPH FOR ARCHIMEDES

267

◦ colorbox = false suppresses the color box on the right side of the graph, ◦ proportional_axes = xyz forces the use of equally-scaled axes. All of these default options are set by issuing the following command:1 set_draw_defaults(proportional_axes = xyz, enhanced3d = true, wired_surface = true, palette = [blue, light_blue], color = black, colorbox = false) $ These graphic constructors can be used to draw points, curves, and surfaces on a three-dimensional canvas. For instance, we can plot a sequence of isolated points as follows: draw3d(proportional_axes = none, point_type = filled_circle, points([[0,0,2], [0,1,0], [0,2,2], [1,2,0], [2,2,2], [2,1,0], [2,0,-1], [1,0,0], [0,0,2]]));

On the other hand, by adding the option points_joined = true, we can render a piecewise-linear curve:

1 Mac OS users may also need to include the option terminal = qt as part of this

command.

268

3. GRAPHICS AND VISUALIZATION

draw3d(proportional_axes = none, point_type = filled_circle, points_joined = true, points([[0,0,2], [0,1,0], [0,2,2], [1,2,0], [2,2,2], [2,1,0], [2,0,-1], [1,0,0], [0,0,2]]));

Finally, we can create a locally-flat surface made up of folded quadrilaterals by calling on the mesh() constructor: draw3d(proportional_axes = none, mesh([[0,0,2],[0,1,0],[0,2,2]], [[1,0,0],[1,1,2],[1,2,0]], [[2,0,-1],[2,1,0],[2,2,2]]));

3.2. AN EPITAPH FOR ARCHIMEDES

269

In a similar way, the constructor parametric() may be used to create smooth parametric curves like the following trefoil knot: draw3d(proportional_axes = none, nticks = 100, parametric((3 + cos(3*theta/2))*cos(theta), (3 + cos(3*theta/2))*sin(theta), sin(3*theta/2), theta, 0, 4*%pi));

We can also create tubular surfaces thanks to the tube() constructor: draw3d(proportional_axes = none, xu_grid=50, yv_grid=10, tube((3 + cos(3*theta/2))*cos(theta), (3 + cos(3*theta/2))*sin(theta), sin(3*theta/2), 0.25, theta, 0, 4*%pi));

270

3. GRAPHICS AND VISUALIZATION

Observe that the syntax for these two constructors is similar to the two-dimensional version of parametric(). It consists of three coordinate functions, each given in terms of a free variable (here it is theta), followed by the name of the variable and a pair of bounds; the tube() constructor takes an additional thickness coordinate, which may also be given as a function of the free variable, if needed. Also notice that the options nticks, xu_grid, and yv_grid can be modified to control the smoothness of each plot. The remaining graphic constructors allow us to render various types of surfaces. For example, consider a sphere of unit radius. Since this surface may be described using the implicit equation x2 + y 2 + z 2 = 1, it can be rendered by the implicit() constructor as follows: draw3d(implicit(x^2 + y^2 + z^2 = 1, x, -1, 1, y, -1, 1, z, -1, 1));

Alternatively, the unit sphere can be described by a pair of explicit equations  z = ± 1 − x2 − y 2 and thus can be drawn by the graphic constructor explicit():

3.2. AN EPITAPH FOR ARCHIMEDES

271

draw3d(explicit(sqrt(1 - x^2 - y^2), x, -1, 1, y, -1, 1), explicit(-sqrt(1 - x^2 - y^2), x, -1, 1, y, -1, 1));

Observe that this plot includes a flat rectangular belt around the sphere, where the square roots are imaginary. We can instruct Maxima to not plot those points by setting draw_realpart = false. Unfortunately, the result is a slightly incomplete version of our sphere: draw3d(draw_realpart = false, explicit(sqrt(1 - x^2 - y^2), x, -1, 1, y, -1, 1), explicit(-sqrt(1 - x^2 - y^2), x, -1, 1, y, -1, 1));

272

3. GRAPHICS AND VISUALIZATION

We can often describe a surface with a set parametric equations in two variables, in which case it can be drawn by the graphic constructor parametric_surface(). For example, a common parametrization for the unit sphere consists of three equations: ⎧ ⎪ ⎪ ⎨x(u, v) = cos(u) · sin(v) y(u, v) = sin(u) · sin(v) ⎪ ⎪ ⎩z(u, v) = cos(v). Here, the parameter u describes longitude and can go from 0 to 2π radians, while the parameter v describes latitude and can range from 0 at the north pole to π at the south pole. This parametrization can be graphed as follows: draw3d(parametric_surface(cos(u)*sin(v), sin(u)*sin(v), cos(v), u, 0, 2*%pi, v, 0, %pi));

The parametrization above is closely related to two coordinate systems that are the natural generalizations of polar coordinates in two dimensions. The first of these systems is called cylindrical coordinates and consists of assigning to each point in three-dimensional space a triple of numbers (r, θ, z) that relate to the standard rectangular triple (x, y, z) by the dictionary of formulas: x = r cos(θ)

r 2 = x2 + y 2

y = r sin(θ)

tan(θ) = y/x.

3.2. AN EPITAPH FOR ARCHIMEDES

273

Observe that the θ coordinate plays the same role as the longitudinal parameter u, while r and z give a point’s distance from the z-axis and the xy-plane, respectively. In particular, all points with a fixed value of r > 0 will describe a cylinder, hence the name of the coordinate system. Since we can describe the unit sphere by the cylindrical equation  r(θ, z) = 1 − z 2 , we may plot it using the constructor cylindrical() as follows: draw3d(cylindrical(sqrt(1 - z^2), z, -1, 1, theta, 0, 2*%pi));

The second coordinate system is known as spherical coordinates. In this case each point in three-dimensional space is assigned a triple (ρ, θ, φ) which is related to the standard rectangular triple (x, y, z) by the formulas: x = ρ cos(θ) sin(φ)

ρ2 = x2 + y 2 + z 2

y = ρ sin(θ) sin(φ)

tan(θ) = y/x  cos(φ) = z/ x2 + y 2 + z 2 .

z = ρ cos(φ)

As before, θ plays the role of the longitudinal parameter u, while φ plays the role of the latitudinal parameter v; finally, ρ gives a point’s distance from the origin. Thus, all points with a fixed value of ρ > 0 describe a sphere, again explaining the name of the coordinate system. In particular, the unit sphere can also be described by the spherical

274

3. GRAPHICS AND VISUALIZATION

equation ρ(θ, φ) = 1, which we may plot using the graphic constructor spherical() as follows: draw3d(spherical(1, theta, 0, 2*%pi, phi, 0, %pi));

We have now examined all of the tools necessary to produce our diagram of a sphere inscribed in a cylinder. As with the sphere above, to render a cylinder of unit radius, we have the choice of equations to graph. For instance, we may ask Maxima to plot the implicit equation x2 + y 2 = 1, or the parametric equations ⎧ ⎪ x(u, v) = cos(u) ⎪ ⎪ ⎨ y(u, v) = sin(u) ⎪ ⎪ ⎪ ⎩z(u, v) = v, or the cylindrical coordinate equation r(θ, z) = 1, or the spherical coordinate equation ρ(θ, φ) = csc(φ),

3.2. AN EPITAPH FOR ARCHIMEDES

275

or the tube with radius 1 about the parametric line x(t) = 0,

y(t) = 0,

z(t) = t.

Let us choose the simplest of these approaches: draw3d(cylindrical(1, theta, 0, 2*%pi, z, -1, 1));

Now, it turns out that Archimedes proved his famous relationship between the volumes of the sphere and the cylinder by comparing each of these figures to a cone with the same height and radius. Indeed, by examining their cross-sectional areas, he was able to show that the volumes of the cone, the sphere, and the cylinder are in a 1 : 2 : 3 ratio. Thus, it seems reasonable for us to also include copy of this cone as part of our figure. As before, we have several options for drawing such a cone. For example, we might plot the implicit equation 4(x2 + y 2 ) = (1 − z)2 , or the explicit equation  z = 1 − 2 x2 + y 2 , or the parametric equations ⎧ ⎪ x(u, v) = v · cos(u) ⎪ ⎪ ⎨ y(u, v) = v · sin(u) ⎪ ⎪ ⎪ ⎩z(u, v) = 1 − 2v,

276

3. GRAPHICS AND VISUALIZATION

or the cylindrical coordinate equation r(θ, z) =

1 2

(1 − z) ,

or the spherical coordinate equation 1 . 2 sin(φ) + cos(φ) Once again, the simplest choice appears to be the cylindrical equation: ρ(θ, φ) =

draw3d(cylindrical((1-z)/2, theta, 0, 2*%pi, z, -1, 1));

In order to produce our homage to Archimedes, we shall draw the cone, sphere, and cylinder together on the same plot, but this time with no axes or label marks; this is easily done by modifying the values of the options axis_3d, xlabel, ylabel, and zlabel as follows: draw3d(axis_3d = false, xlabel = "", ylabel = "", zlabel= "", cylindrical((1-z)/2, theta, 0, 2*%pi, z, -1, 1), spherical(1, theta, 0, 2*%pi, phi, 0, %pi), cylindrical(1, theta, 0, 2*%pi, z, -1, 1));

3.2. AN EPITAPH FOR ARCHIMEDES

277

This initial result is somewhat unsatisfactory because, at least from Maxima’s default vantage point, the cylinder blocks most of the sphere and all of the cone. If we rotate the figure on an external plot window and view it from below, then we will be able to see portions of the sphere and the cone. However, we can do better. We can improve the visibility of our figure by making several adjustments. First of all, we can switch our plotting options to allow for transparent surfaces, so that we can see the sphere and cone through the sides of the cylinder. Secondly, we can add little color, say by rendering the cone in blue, the sphere in green, and the cylinder in red. We can also reduce the number of gridlines used to render each surface. As we saw earlier, Maxima provides two options to do this, xu_grid and yv_grid, each with a default value of 30. In our case, a little experimentation reveals that setting xu_grid to 5 lines for the cylinder, 25 lines for the sphere, and 15 lines for the cone produces a pleasing visual effect. Finally, for the coup de grace, we call on the option title to give our homage an appropriate message: draw3d(enhanced3d = false, surface_hide = false, xlabel = "", ylabel = "", zlabel= "", axis_3d = false, title = "Noli turbare circulos meos -- Archimedes", color = blue, xu_grid = 15, cylindrical((1-z)/2, z, -1, 1, theta, 0, 2*%pi),

278

3. GRAPHICS AND VISUALIZATION

color = green, xu_grid = 25, spherical(1, theta, 0, 2*%pi, phi, 0, %pi), color = red, xu_grid = 5, cylindrical(1, z, -1, 1, theta, 0, 2*%pi));

Exercises for Section 3.2 1.

(a) Verify that the five equations given in page 274 describe a cylinder of unit radius by plotting each one using the appropriate graphic constructor with a suitable choice of inputs. (b) Verify that the five equations given in page 275 describe the same cone by plotting each one using the appropriate graphic constructor with a suitable choice of inputs.

2. A torus is a surface shaped like an inner tube or a doughnut like the one shown below:

(a) One way to construct a torus is to wrap a cylindrical tube around a horizontal circle. For example, we can start with the circle x2 + y 2 = 4

z=0

and wrap a tube of radius 1 around it. This will create a torus with a maximum radius of 3 at its widest point and a minimum radius of 1 in the narrowest part of the doughnut hole. Use the tube() graphic constructor to produce a picture of this torus. (b) Another way to construct a torus is to rotate a vertical circle about the z-axis. For instance, we can produce the torus described above by rotating the circle (x − 2)2 + z 2 = 1

y=0

about the z-axis. Find a triple of parametric functions for this surface and plot them using the parametric_surface() graphic constructor. (c) A third approach for constructing a torus is to consider the equations for its longitudes and meridians, that is to say, for the horizontal and vertical circles winding around the torus. For instance, in the torus described above, a typical

280

3. GRAPHICS AND VISUALIZATION

longitude is described by the equation x2 + y 2 = r 2 , while a typical meridian is described by the equation (r − 2)2 + z 2 = 12 . Merge these into a single implicit equation and plot the resulting surface using the implicit() graphic constructor. 3. The graph of the function z(x, y) = sin(2πx) on the domain 0 ≤ x ≤ 29 should go through twenty-nine full oscillations, but Maxima only shows one: draw3d(explicit(sin(2*%pi*x), x, 0, 29, y, 0, 1));

This effect is known as aliasing because it allows one function to masquerade as another. (a) Explore the effects of aliasing when the domain of the function above varies from 0 ≤ x ≤ 27 to 0 ≤ x ≤ 34. In each case, compare the number of oscillations that should be displayed with the number of oscillations that actually appear in the graph. (b) Compare the effects of aliasing in part (a) to those visible when the domain is changed to 0 ≤ x ≤ 14 or 0 ≤ x ≤ 16. What qualitative differences do you observe in these two graphs? Do they show the correct number of oscillations? (c) The effects of aliasing are a result of poor sampling, which can be controlled either by increasing the value given to the option xu_grid or by decreasing the size of the domain. For the function above, what is the largest domain that completely avoids aliasing?

EXERCISES

4.

281

(a) Render a three-dimensional model of the flower shown below by plotting the following components together in a single graph.

◦ For the stem, plot the tube of radius 0.1 given by the equations ⎧ ⎪ ⎪ ⎨x(t) = 0.3 sin(1.5t) y(t) = 0.3 cos(1.5t) ⎪ ⎪ ⎩z(t) = t where −3 ≤ t ≤ 2.5. ◦ For the stamen, plot the implicit equation x2 + y 2 + (z − 2.5)2 = 0.5 with −1 ≤ x ≤ 1, −1 ≤ y ≤ 1, and 1.5 ≤ z ≤ 3.5. ◦ For the petals, plot the cylindrical equation √

r(θ, z) =

z (3 − z)0.2

with 0 ≤ z ≤ 2.9 and 0 ≤ θ ≤ 2π. (b) Design a parametric surface that can serve as a vase for the flower in part (a). Your vase should have a relatively flat bottom, a wide body, a narrow neck, and a flared top. Use Maxima to display your vase, along with the flower. You may want to give your flower a longer stem so it can stay in the vase.

282

3. GRAPHICS AND VISUALIZATION

5.

(a) The following list contains the vertices of a solid cube: cube : [[1, 1, 1], [1, 1, -1], [1, -1, 1], [1, -1, -1], [-1, 1, 1], [-1, 1, -1], [-1, -1, 1], [-1, -1, -1]] $ Use the quadrilateral() graphic constructor to build a three-dimensional plot of this solid. This constructor takes an input of four points that are used as the corners of the quadrilateral. Use the color and line_width options to create a figure with white faces and thick, colored edges. (b) The following list contains the vertices of a solid with eight triangular faces known as an octahedron (see page 263 for an illustration): octa : [[1, 0, 0], [0, 1, 0], [0, 0, 1], [-1, 0, 0], [0, -1, 0], [0, -0, 1]] $ Use the triangle() graphic constructor to build a threedimensional plot of this solid. This constructor has similar syntax to quadrilateral(), but only takes an input of three points. Use the color and line_width options to create a figure with white faces and thick, colored edges.

3.3. Rabbits Revisited The reason that Fibonacci’s model for the rapid growth of rabbits is so widely loved is simple: The rabbits never die. While this is great news for lovers of all creatures great and small, it spells certain doom for the finances at the Fibonacci estate, which will be forced to commit greater and greater resources to housing and feeding their ever growing family of bunnies. No doubt, it is a fairy-tale finish for the furry-tailed friends, but it is not one that often plays out in real life. In this section, we consider a somewhat more realistic way to model a population of rabbits. We start by imagining that our rabbits inhabit a lush tropical island full of delicious green leafy vegetation. Unfortunately, the island cannot support their voracious appetite indefinitely, so there is a maximum number of rabbits that can thrive there. In technical terms, this is known as the island’s carrying capacity. When there are only a few rabbits on the island, there is plenty of food to go around and the population can grow uninhibitedly, that is to say, like rabbits. However, when the number of rabbits

rabbits

carrying capacity

time Fig. 3-3. In the absence of predators, the population of rabbits will grow logistically towards the island’s carrying capacity.

3. GRAPHICS AND VISUALIZATION

foxes

284

time Fig. 3-4. In the absence of prey, the population of foxes will decay exponentially. approaches the carrying capacity, there is more competition for food and the population growth slows down, as shown in Fig. 3-3. This type of S-shaped graph is known as a logistic curve. There is more bad news for the rabbits: The island is also inhabited by a pack of ravenous foxes, who would love nothing more than to have the rabbits for lunch. Whenever a fox meets up with a rabbit, a life-ordeath chase will inevitably ensue, and although not every encounter will result in a casualty for the rabbits and a tasty morsel for the foxes, the more rabbits and foxes there are, the more likely it is that there will be one fewer rabbit going home at the end of the day. Before we start cheering for the rabbits, we need to look at the world through the foxes’ eyes. They, too, need to eat, and their canine teeth are not designed for the tasteless green leafy vegetation that the island has to offer. Indeed, if it were not for the presence of those scrumptious rabbits, the fox population would decay exponentially away to extinction in no time flat, as illustrated in Fig. 3-4. However, foxes are sly, and they can eke out a living from just a few lucky encounters with a rabbit. The more rabbits and foxes there are, the more likely it is that there will be some satisfied fox cubs going to bed with full tummies at the end of the day.

3.3. RABBITS REVISITED

285

We can therefore summarize the situation on the island as follows: x The population of rabbits will grow logistically in the absence of foxes, but it will decrease every time that a fox catches a rabbit. y The population of foxes will decay exponentially in the absence of rabbits, but it will increase at a rate proportional to the number of times that a fox catches a rabbit. z The number of times that a fox catches a rabbit is proportional to the number of rabbits multiplied times the number of foxes. Supposing, then, that the number of rabbits on the island is given by r(t), and that the number of foxes on the island is given by f (t), we can rewrite the statements above as the following system of differential equations: ⎧ ⎨r (t) = 0.1 r − 0.000 01 r2 − 0.005 rf

rabbits per month

⎩f (t) = 0.000 04 rf − 0.04 f

foxes per month.



These equations describe a variant of the LotkaVolterra model , which was proposed independently by Alfred Lotka (1880 – 1949) and Vito Volterra (1860 – 1940) to model the dynamics of predator-prey interactions among different species of fish in the Adriatic Sea, lynx and snowshoe hares in Hudson Bay, and moose and wolves in Isle Royale in Lake Superior. The five coefficients in these equations depend on field measurements made by biologists studying the rabbits and foxes on our island; they describe the growth and decay rates of the two populations, the carrying capacity for the island, and the probability that any given encounter between a rabbit and fox will turn deadly. In particular, each term containing the product r · f corresponds to the effect produced when a fox catches a rabbit, while the terms involving only r or only f correspond to the logistic or exponential behavior that each population experiences in the absence of the other. We can think of the differential equations as describing how the two populations are likely to change in the future based on their present values. For instance, since each derivative is measured in terms of individuals per month, they can help us project how many new rabbits or foxes we expect to see in a month’s time. We can compute these derivatives in Maxima by entering the following definitions:

286

3. GRAPHICS AND VISUALIZATION

rprime(r, f) := 0.1*r - 0.00001*r^2 - 0.005*r*f $ fprime(r, f) := 0.00004*r*f - 0.04*f $ Suppose that we know that at present there are 2 200 rabbits and 16 foxes; we will say that these are the values of r and f at time t = 0: r : 2200; f : 16; 2200 16 Assuming that each population changes at about the same rate for the foreseeable future, we can predict that after some time Δt, their new values will be approximately: rnew = rold + Δr ≈ rold + r · Δt, fnew = fold + Δf ≈ fold + f  · Δt. For example, let us look ahead one month to time t = 1. This corresponds to a change in time of Δt = 1, so our model predicts that each population will change as follows: deltar : rprime(r, f); deltaf : fprime(r, f); −4.400000000000006 0.7680000000000001 r : r + deltar; f : f + deltaf; 2195.6 16.768 Evidently, over the course of the next month, the number of rabbits will go down by about four while the number of foxes will increase slightly. However, we might take pause to question how to interpret these values. What are we to make of six tenths of a rabbit or seventy seven percent of a fox? In both these cases, we should remind ourselves that we are working with a mathematical model which is only meant to give an approximate reflection of reality. In particular, the

3.3. RABBITS REVISITED

287

model requires that we treat the number of rabbits or foxes as continuous real-valued quantities whereas we know that they are really discrete integer-valued quantities. With these updated values of r and f in hand, we can forge ahead and predict the size of the two populations after another month, when t = 2. Once again, our change in time is Δt = 1, so we obtain: deltar : rprime(r, f); deltaf : fprime(r, f); −12.72569759999999 0.8019128319999999 r : r + deltar; f : f + deltaf; 2182.8743024 17.569912832 Then, by repeating the process one more time, we can predict what the situation will be like the month after that, when t = 3: deltar : rprime(r, f); deltaf : fprime(r, f); −21.12652804268691 0.8313199353752323 r : r + deltar; f : f + deltaf; 2161.747774357313 18.40123276737523 Note that in three months’ time, the number of rabbits has dropped by almost 2%. The foxes are clearly taking their toll, and with two new fox cubs around, things do not look much better for the future! The computations above demonstrate a simple example of a general algorithm known as Euler’s method , named after the famous mathematician Leonhard Euler. This algorithm can find a numerical approximation to the solution of a system of differential equations like the one describing the rabbit and fox populations. In our example, we used time steps of size Δt = 1, but any other size would do. The underlying assumption is that the derivatives involved remain relatively stable over the course of each step of the computation. However, since

288

3. GRAPHICS AND VISUALIZATION

Table 3-6. Twelve useful printf() directives. directive

result

~A

standard printing

~B

binary integer

~D

decimal integer

~E

scientific notation

~F

floating-point number

~G

generic floating-point

~R

spelled out integer

~:R

spelled out ordinal

~@R

roman numeral

~X

hexadecimal integer

~%

new line

~{ · · · ~}

iteration over a list

these derivatives are not likely to be constant, we get better approximations by taking lots of small steps instead of a few long ones. We will now develop an implementation of Euler’s method in which move forward one unit of time by taking n time steps of size Δt = n1 . Thus, the larger the value of n, the more accurate the results. Our goal will be to produce a table of values showing the size of the rabbit and fox populations on a given month. To avoid a bulky display of floating-point numbers, we will turn to the formatted printing command printf(). As we shall see, this is a very powerful and versatile command and we will barely scratch the surface on how it can be used. The basic syntax includes a destination keyword (which we will usually take to be true), a control sequence in quotation marks, and some number of additional inputs to display. The control sequence can consist of any combination of text and special directives controlling how the output is to be displayed. Table 3-6 lists the twelve different directives that we will examine. The simplest of these directives is ~A. This instructs Maxima to take the next input and insert it in place of the directive. We can combine it with ~% to advance the display to the next line like a carriage return. For example, we might enter:

3.3. RABBITS REVISITED

289

printf(true, "Rabbits are ~A.~%Foxes are ~A.", cute, sly) $ Rabbits are cute. Foxes are sly. You might wish to supply Maxima with your own choice of adjectives to use instead, perhaps to show your partiality towards one group of animals or the other. The next directives of note are the pair ~{ and ~}. These assume that the next input is a list, and proceed to apply the control sequence they enclose to each and every item in that list. For instance, the following command makes an inventory of the contents of its input: printf(true, "This list contains: ~% ~{ ~A ~% ~}", [12, 3.4e5, time, [rabbits, foxes]]) $ This list contains: 12 340000.0 time [rabbits,foxes] The directives ~D, ~B, and ~X all take integer inputs and display them, respectively, in our usual decimal, binary, and hexadecimal (base 16) notations: printf(true, "~D ~B ~X", 23, 45, 678) $ 123 101101 2A6 The directives ~R, ~:R, and ~@R also take integer inputs. The first two display their inputs by spelling out their names in English, either as cardinal or ordinal numbers; the third displays its inputs as Roman numerals: printf(true, "~R ~:R ~@R", 23, 45, 678) $ twenty-three forty-fifth DCLXXVIII Of particular importance to us are the directives ~E, ~F, and ~G, which take floating-point numbers as inputs. The directive ~E always displays its inputs using scientific notation, while ~F always displays its inputs in the standard decimal-point format. The general purpose directive ~G chooses between these two styles depending on the size

290

3. GRAPHICS AND VISUALIZATION

of the number in question: printf(true, "~{~E ~F ~G ~%~}", [77.77, 77.77, 77.77, 8.88e-2, 8.88e-2, 8.88e-2]) $ 7.777e+1 77.77 77.77 8.88e−2 0.0888 8.8800e−2 We can change the number of digits displayed by these directives by modifying them as follows: printf(true, "~{~8,3E ~8,3F ~8,3G ~%~}", [77.77, 77.77, 77.77, 8.88e-2, 8.88e-2, 8.88e-2]) $ 7.777e+1 77.770 77.8 8.880e−2 0.089 8.880e−2 Observe that, in the case of ~E and ~F, the additional specifications instruct Maxima to display each floating-point number using eight symbols, with exactly three digits after the decimal point. Thus, by displaying numbers with matching specifications, we can create slim and nicely-formatted tables in which the decimal points all line up one under the other, regardless of the size of the numbers involved. For example: printf(true, "~{~{ ~7,1F ~}~%~}", [[1, 2.2, 3.33], [4.444, 55.5, 66.66], [77.777, 888.8, 9999]]) $ 1.0 2.2 3.3 4.4 55.5 66.7 77.8 888.8 9999.0 This is precisely how we want to display the changing populations of rabbits and foxes. Our implementation of Euler’s method will take as input the initial number of rabbits and foxes, the number of months for which we wish to create a population forecast, and an integer n. As we noted earlier, our computation will take time steps of size Δt = n1 . However, for the sake of brevity, our function will only print out the values of r and f after every n steps, that is, at the end of each month. This is accomplished by nesting two for loops, one inside another, as follows:

3.3. RABBITS REVISITED

291

Euler(r, f, tfinal, n) := block([deltar, deltaf, deltat, t, j], deltat : 1/n, printf(true, " time rabbits foxes ~%"), printf(true, "~{ ~7,1f ~} ~%", [0, r, f]), /* nested loops repeat Euler’s Method */ for t : 1 thru tfinal do (for j : 1 thru n do (deltar : rprime(r, f) * deltat, deltaf : fprime(r, f) * deltat, r : r + deltar, f : f + deltaf), /* print results after n steps = 1 month */ printf(true, "~{ ~7,1f ~} ~%", [t, r, f]))) $ Note that the printf() command in the third line of our definition has four spaces before the word “time,” two before the word “rabbits,” and four before the word “foxes” to guarantee that the titles in the resulting table are properly aligned. To test our implementation, we forecast the size of our rabbit and fox populations over the course of first five months by entering: Euler(2200, 16, 5, 1) $ time rabbits foxes 0.0 2200.0 16.0 1.0 2195.6 16.8 2.0 2182.9 17.6 3.0 2161.7 18.4 4.0 2132.3 19.3 5.0 2094.8 20.1 According to our model, more than a hundred rabbits will have fallen prey to the foxes by the end of the fifth month.

292

3. GRAPHICS AND VISUALIZATION

This computation works under the assumption that r (t) and f  (t) will remain constant for an entire month at a time. However, since both of these derivatives are continually changing, we will get a better prediction if repeat the computation using a smaller time step. For instance, we can compute a second forecast using a time step of 1 Δt = 10 as follows: Euler(2200, 16, 5, 10) $ time rabbits foxes 0.0 2200.0 16.0 1.0 2191.8 16.8 2.0 2175.3 17.6 3.0 2150.4 18.4 4.0 2117.4 19.3 5.0 2076.7 20.2 Note that this second computation predicts even heavier losses for the rabbits! The differences in the two forecasts above illustrate one of the main problems with Euler’s method: Each step in the method assumes that the rates of change in question remain constant for some period of time. In most cases, this is simply not true, and some small amount of error creeps into the computation. As the number of steps increases, these small errors tend to add up, creating a much larger cumulative error in our final answer. The usual way to reduce the cumulative error is to decrease the step size Δt. In fact, one can prove that under typical conditions the cumulative error in Euler’s method is proportional to Δt. This means that we can expect the error in the second forecast above to be about one tenth that of the first; of course, the second forecast took ten times as long to produce. To obtain an even better forecast, we could use a smaller step size, but then again, this will increase the time it takes to complete the computation. There are better approaches to reduce the cumulative error in Euler’s method. These involve more complicated computations that produce better approximations without significantly increasing the running time. One of the most famous of these approaches was developed around the turn of the twentieth century by the German mathematicians Carl Runge (1856 – 1927) and Martin Kutta (1867 – 1944). Each step in Runge-Kutta method takes about four times as long to complete as a step in Euler’s method. However, under typical conditions

3.3. RABBITS REVISITED

293

the cumulative error produced by Runge-Kutta is roughly proportional to Δt 4 . The Maxima function rk() implements the Runge-Kutta method automatically for us. The syntax for this function consists of a list of differential equations, a list of the corresponding dependent variables, a list of their initial values, and finally a list containing the name of the independent time variable along with its initial and final value and the step size to be used. For example, we can obtain a forecast for the populations of rabbits and foxes over an entire year, displayed in a smart-looking table, by entering the following commands: kill(r, f) $ rksoln : rk([rprime(r, f), fprime(r, f)], [r, f], [2200, 16], [t, 0, 12, 1]); [[0.0, 2200.0, 16.0], [1.0, 2191.411387203374, 16.78431633746431], ··· [12.0, 1634.765656277808, 25.72693507752372]] printf(true, " time rabbits foxes ~%") $ printf(true, "~{~{ ~7,1f ~}~%~}", rksoln) $ time rabbits foxes 0.0 2200.0 16.0 1.0 2191.4 16.8 2.0 2174.4 17.6 3.0 2149.1 18.4 4.0 2115.8 19.3 5.0 2074.8 20.2 6.0 2026.7 21.0 7.0 1972.2 21.9 8.0 1912.1 22.7 9.0 1847.4 23.5 10.0 1778.9 24.3 11.0 1707.7 25.0 12.0 1634.8 25.7 According to this latest forecast, the situation looks pretty grim for the rabbits, who in a year’s time will see their numbers cut by more than 25%. On the other hand, the foxes are promised a feast of

294

3. GRAPHICS AND VISUALIZATION

a time, with their population rising by more than 60%. Of course, the good times cannot last forever: If the rabbits are hunted to extinction, the foxes are sure to follow! To better visualize the long-term forecast for our island, we can graph both populations as functions of time. For instance, we can call on the function rk() to estimate the number of rabbits and foxes over the next 250 months using time steps of Δt = 0.1. The result will consist of a rather unwieldy list of triples [t, r, f ], amounting to a total of some 7 500 floating-point numbers. Undoubtedly, we do not want to see this list in its raw form so we use a dollar sign to suppress its output: rksoln : rk([rprime(r, f), fprime(r, f)], [r, f], [2200, 16], [t, 0, 250, 0.1]) $ Next, we construct a new list of ordered pairs of the form [t, r], representing the population of rabbits as it changes over time, again using a dollar sign to suppress Maxima’s output: rpts : makelist([rksoln[j][1], rksoln[j][2]], j, 1, length(rksoln)) $ Finally, we create a piecewise-linear graph by plotting our ordered pairs with the points() graphic constructor. We take advantage of the options xlabel and ylabel to indicate that one axis represents time and the other rabbits: wxdraw2d(point_type = dot, points_joined = true, grid = true, xlabel = "time", ylabel = "rabbits", color = blue, points(rpts));

3.3. RABBITS REVISITED

295

We can repeat this procedure to construct and graph a list of ordered pairs [t, f ], representing the population of foxes as a function of time: fpts : makelist([rksoln[j][1], rksoln[j][3]], j, 1, length(rksoln)) $ wxdraw2d(point_type = dot, points_joined = true, grid = true, xlabel = "time", ylabel = "foxes", color = red, points(fpts));

We can now plot our two populations together in a single graph. Before we do, however, you should note that the values for r(t) tend to

296

3. GRAPHICS AND VISUALIZATION

be several orders of magnitude greater than those for f (t). This means that, if graphed on the same scale as the rabbits, it would be difficult to discern any variations in the fox population. We solve this problem by plotting the rabbit and fox populations using different scales, one marked along the left side and the other along the right side of the graph. The following graphic options will help us accomplish this task: ◦ yaxis_secondary = true will set up the second scale along the right side of the graph; this option should appear before the graphic constructor that corresponds to this second scale. ◦ yrange_secondary determines the maximum and minimum values to be displayed along the second scale. ◦ ylabel_secondary places a label describing the second scale. Should you ever need them, Maxima also provides the options to set up the second scale along the top side of the graph: xaxis_secondary, xrange_secondary, and xlabel_secondary. The following command produces our combined graph for the populations, adjusting both vertical scales so they each start at zero: wxdraw2d(point_type = dot, points_joined = true, grid = true, xlabel = "time", ylabel = "rabbits", yrange = [0, 2200], color = blue, key = "rabbits", points(rpts), yaxis_secondary = true, ytics_secondary = true, ylabel_secondary = "foxes", yrange_secondary = [0, 30], color = red, key = "foxes", points(fpts));

3.3. RABBITS REVISITED

297

Our long-term projections finally bring a silver lining amidst the dark clouds surrounding our rabbits. Evidently, their numbers will continue to dwindle for about four years, at which point their fortunes will turn and they will experience a growth spurt. The reason for their sudden stroke of luck is that the population of foxes at that point will be on its way down, no doubt due to poor hunting. The process will then continue in a wavelike pattern marked by an increase in the number of rabbits when there are few foxes, an increase in the number of foxes when there are many rabbits, a drop in rabbits when there are many foxes, and a drop in foxes when there are few rabbits. The cycle repeats every nine or ten years. Note, however, that the pattern of fluctuations seems to be dying out as the graph reaches a lower maximum value and a higher minimum value after each cycle. We can get a better sense of this behavior by treating the two populations as a pair of parametric equations, rather than as functions of time. We start by creating an even longerterm forecast, from which we extract and plot pairs [r, f ] as follows: rksoln : rk([rprime(r, f), fprime(r, f)], [r, f], [2200, 16], [t, 0, 1000, 1]) $ pts : makelist([rksoln[j][2], rksoln[j][3]], j, 1, length(rksoln)) $

298

3. GRAPHICS AND VISUALIZATION

wxdraw2d(point_type = dot, points_joined = true, grid = true, xlabel = "rabbits", ylabel = "foxes", color = dark_violet, points(pts));

This type of graph is called a phase-space plot for the system of populations in the island. Each point represents a different state of this system, and the curve we plotted indicates how the system has changed states over time. For instance, at the outset, the system’s state corresponds to a point on the far right of the graph. Then, as time passes and the populations of rabbits and foxes fluctuate, the system moves along a counterclockwise spiral. In time, our the system will get closer and closer to the state at the center of the spiral. This is known as the steady state of the system because, once the system reaches this state, the populations will not fluctuate anymore. The presence of a steady state guarantees that in the long run neither population is heading towards extinction, and that is very good news for rabbits and foxes alike!

Exercises for Section 3.3 1. Consider the populations of rabbits and foxes described by the equations at the bottom of page 285. Use Maxima’s symbolic computation engine to find the values of the following parameters: (a) The carrying capacity of the island, that is, the number of rabbits that can live there in the absence of foxes. Hint: Solve for r when f = 0 and r = 0. (b) The exponential decay rate for the population of foxes in the absence of rabbits. Hint: Solve for k = −f  /f when r = 0 and r = 0. (c) Every steady state of the system, that is, the number of rabbits and foxes for which the system is stable. Hint: Solve for r and f when r = 0 and f  = 0. 2. The functions drawdf() and wxdrawdf() draw a direction field for a dynamical system consisting of one or two ordinary differential equations. In order to use these functions, you must first load the drawdf package by entering: load(drawdf) $ We can then draw the direction field corresponding to the populations of rabbits and foxes described by the equations in page 285 with the command: wxdrawdf([0.1*r - 0.00001*r^2 - 0.005*r*f, 0.00004*r*f - 0.04*f], [r, 0, 2500], [f, 0, 30], xlabel = "rabbits", ylabel = "foxes") $

300

3. GRAPHICS AND VISUALIZATION

The special graphic constructor point_at() instructs Maxima to plot a given point in the direction field, while soln_at() draws the system’s trajectory (looking both forwards and backwards in time) through a point. Each of these can be modified by a variety of drawing options. For instance, we can produce a phase-space plot for this system by graphing its trajectory through a point like r = 2200, f = 16 as follows: wxdrawdf([0.1*r - 0.00001*r^2 - 0.005*r*f, 0.00004*r*f - 0.04*f], [r, 0, 2500], [f, 0, 30], xlabel = "rabbits", ylabel = "foxes", color = dark_violet, line_width = 2, duration = 250, point_at(2200, 16), soln_at(2200, 16)) $

Graph the direction field for the system above in each of the following ranges. In each case, describe (i) the general shape of the field and (ii) all the different types of trajectories possible within the given range. (a) 900 ≤ r ≤ 1100, 17 ≤ f ≤ 19. (b) −5 ≤ r ≤ 5, −5 ≤ f ≤ 5. (c) 9000 ≤ r ≤ 11000, −1 ≤ f ≤ 1. 3. The Kermack-McKendrick SIR model is a mathematical system that describes the spread of a contagious disease over time. It assumes that the disease in question is sufficiently mild that anyone who falls ill eventually recovers. Furthermore, once someone recovers from it, they will be permanently immune from future infection. Finally, the affected population is assumed to be relatively large but fixed in size, with no births or deaths.

EXERCISES

301

For example, consider the student population of a small liberal arts university. On any given day, students at the university can be divided into three groups: ◦ those who have never had the illness and are now susceptible to catch it; ◦ those who are infected with the illness and are now contagious; ◦ those who have recovered from the illness and are now immune. Let s, i, and r denote the number of people in each of these groups. The aim of the SIR model is to predict how these numbers change over the course of time. In particular, the model consists of the following system of differential equations describing how students progress from the susceptible stage to the infected stage to the recovered stage: ⎧ ⎪ s = −0.001 · s · i ⎪ ⎪ ⎨ 1 i i = 0.001 · s · i − 14 ⎪ ⎪ ⎪ ⎩r = 1 i. 14 In each case, the rate of change is measured in number of students per day. Suppose there are 1000 students at the university, 39 of whom are infected with the disease and 3 of whom have already recovered. According to the model, an increasing number of susceptible students will start to fall sick over the next few days. Using Maxima’s implementation of the Runge-Kutta method to model the spread of this disease, determine how long it will be before the worst part of the epidemic is over and the number of infected students starts to come down again. 4. Consider the Kermack-McKendrick SIR model described in Exercise 3 as it applies to a liberal arts university with 1000 students, 39 of whom are infected and 3 of whom have already recovered. (a) Suppose that a quarantine is imposed at our university separating infected students from the rest of the student population as soon as they present with symptoms. The goal of such a quarantine would be to reduce the number of times that a susceptible student comes in contact with someone who is infected, and consequently the rate of infection s , by a factor of ten. Using Maxima’s implementation of the Runge-Kutta method to model the spread of this disease,

302

3. GRAPHICS AND VISUALIZATION

determine the long-term effects of imposing such a quarantine. (b) Suppose that some fraction of the susceptible population say ten percent, is inoculated with a vaccine each day, thus forming a fourth group v that is immune without ever getting sick. Determine the long-term effects of setting up such an inoculation policy. (c) Modify the equations that govern the spread of the disease to incorporate a two-pronged solution consisting of both a quarantine and an inoculation policy. Determine the longterm effects of this two-pronged solution. 5. As a baseball travels through the air, it experiences a force called drag that slows it down. If we assume that drag is proportional to the square of speed, then we can model the trajectory of the baseball by the following system of differential equations: ⎧ ⎪ x (t) = v ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎨y  (t) = w √ ⎪ v  (t) = −c v v 2 + w2 ⎪ ⎪ ⎪ ⎪ ⎪ √ ⎪ ⎪ ⎩w (t) = −c w v 2 + w2 − g.

Here, x and y give the horizontal and vertical position of the baseball, v and w give the horizontal and vertical components of its velocity, and g = 9.8 and c = 0.0025 are constants describing the forces of gravity and drag, respectively. (a) Use Euler’s method to determine the trajectory of the baseball if it is thrown in the air with an initial speed of 50 m/sec at an angle of θ = 60◦ . (b) Compare the results in part (a) with the frictionless case, in which c = 0. In which situation does the baseball travel farther? In which situation does the baseball “hang” in the air for longer?

3.4. Projectiles in Motion Nearly a century before Newton proved that Kepler’s laws of planetary motion could be explained by the law of gravity, the Italian physicist Galileo Galilei (1564 – 1642) conjectured that, except for the effects of air resistance, all objects near the surface of the earth fall with a constant acceleration, regardless of mass. This was a revolutionary idea that went in the face of the Aristotelian physics that was prevalent at the time. According to scientific lore, just as Newton was inspired by watching an apple fall from a tree, so Galileo demonstrated his hypothesis by dropping balls of different sizes, ranging from small musket balls to large cannon balls, from the Leaning Tower of Pisa. Even though both stories have been told time and time again, going back to the time of the scientists themselves, most scholars believe them to be at best as embellished thought-experiments told by Galileo and Newton to their students and admirers. Our goal in this section is to visualize Galileo’s law of constant acceleration, and in the process derive many of the formulas you might have already studied in a physics class. For the sake of illustration, we shall consider the motion of a compact projectile that it is not subject to air resistance, say, a green juicy apple. At time t = 0, a tiny cannon launches our apple into the air with an initial speed v0 at an angle θ from the horizontal. From that point on, our apple will experience the constant, downward acceleration due to gravity, which we can represent in Maxima as the following vector: a : [0, -g]; [0, −g] We can then recover our apple’s velocity and position by integrating this acceleration vector twice. First we call on Maxima’s integrate() function, which takes as input the formula to be integrated and the name of the variable with respect to which the integration is to be performed. In the first instance, we must also add a constant of integration corresponding to the apple’s initial velocity of v0 (cos(θ), sin(θ)): v : integrate(a, t) + v0 * [cos(theta), sin(theta)]; [cos (θ) v0, sin (θ) v0 − g t] In the second case, we can assume that the apple starts at the origin

304

3. GRAPHICS AND VISUALIZATION

so no constant of integration is required: f : integrate(v, t); [t cos (θ) v0, t sin (θ) v0 −

g t2 ] 2

Observe that the results of the above computations are all formulas. We will want to turn these into functions that we can evaluate with different input values. One way to do this would be to type out each formula on its own, but Maxima provides a quicker alternative by means of the ditto operator. The only twist is that in a function definition, the ditto must be preceded by a pair of apostrophes, as in the following: f[1]; t cos (θ) v0 horizontal(g, v0, theta, t) := ’’%; horizontal (g, v0, θ, t) := t cos (θ) v0 f[2]; t sin (θ) v0 −

g t2 2

vertical(g, v0, theta, t) := ’’%; g t2 vertical (g, v0, θ, t) := t sin (θ) v0 − 2 In this way, we have defined a pair of functions that give the horizontal and vertical position of our projectile. With these, we can draw the trajectory of an apple fired with a given initial speed v0 and angle θ: wxdraw2d(point_type = filled_circle, color = green, point_size = 2, points([[horizontal(9.8, 100, %pi/3, 15), vertical(9.8, 100, %pi/3, 15)]]), color = red, line_type = dots, parametric(horizontal(9.8, 100, %pi/3, t), vertical(9.8, 100, %pi/3, t), t, 0, 15), color = black, label_alignment = left, label(["t = 15.0 seconds", 200, 50]));

3.4. PROJECTILES IN MOTION

305

This graph illustrates a projectile t = 15 seconds after being launched with an initial speed of v0 = 100 meters per second at an angle of θ = π/3 radians under the effects of a gravitational acceleration of 2 g = 9.8 m/sec . The command we used to produce it consists of three graphic constructors: points() to draw the position of the apple, parametric() to draw its trajectory since it left the cannon, and label() to add text to the figure. The syntax for this last constructor is a list containing the text to be inserted and a pair of coordinates indicating where it should be placed. Our first objective will be to convert this stationary picture into an animation that can be controlled using the “slider button” at the top of the Maxima window. Our animation will actually consist of some number of frames, like the one pictured above, except that the position of the apple will be computed at different points in its trajectory, starting from the moment it leaves the cannon all the way until it hits the ground some time later. By displaying the images in quick succession, our eyes will be tricked into filling the gaps between the frames, creating a sense of motion.2 In order to create our animation, we will first find the duration of the projectile’s flight by determining when its vertical position is zero. We can use the solve() command to do this:

2 This is essentially how motion pictures and television work; in those media,

images flash at a rate of approximately 24 frames per second.

306

3. GRAPHICS AND VISUALIZATION

solve(vertical(g, v0, theta, t) = 0, t); 2 sin (θ) v0 , t = 0] [t = g rhs(%[1]); 2 sin (θ) v0 g duration(v0, theta) := ’’%; 2 sin (θ) v0 duration (g, v0, θ) := g Next, we will divide the duration of the flight into 20 equal pieces and make a list of time values to use in our animation: times : float(makelist(i * duration(9.8, 100, %pi/3)/20, i, 0, 20)); [0.0, 0.8836993916167739, 1.767398783233548, ··· 15.90658904910193, 16.7902884407187, 17.67398783233548] Finally, we modify the wxdraw2d() command used to produce the single frame above, and turn it into a with_slider_draw() command. The first two inputs required by this command are the name of a free variable that will serve as a “frame counter” and a list of values to use for it; the rest are drawing instructions that describe the contents of each frame in terms of the frame counter. In our case, the frame counter x will refer to the elapsed time since the apple was launched. We will thus need to adjust the inputs used for the points(), parametric(), and label() constructors, replacing the four instances in which we used the specific time of t = 15 seconds with the generic value of x. Of particular note is the text in the label() constructor, which is replaced with a printf() command that displays the elapsed time to the nearest tenth of a second. The modified command will appear as follows: with_slider_draw(x, times, point_type = filled_circle, color = green, point_size = 2, points([[horizontal(9.8, 100, %pi/3, x), vertical(9.8, 100, %pi/3, x)]]),

3.4. PROJECTILES IN MOTION

307

color = red, line_type = dots, parametric(horizontal(9.8, 100, %pi/3, t), vertical(9.8, 100, %pi/3, t), t, 0, x), color = black, label_alignment = left, label([printf(false, "t = ~5,1f seconds", x), 200, 50])); In response, Maxima will produce a single inline image showing the first frame of the animation. To see the rest of the frames, you should click on the image and press the triangular icon next to the slider at the top of the Maxima window. Alternatively, you can use the slider itself to move forwards or backwards through the animation. Unfortunately, you probably found this first animation somewhat underwhelming since the frames were far too different from one another and they changed much too slowly to create the illusion of motion. To correct this, we will first speed up our animation’s frame rate so it displays 10 frames per second: wxanimate_framerate : 10 $ Next, we will adjust the values of xrange and yrange so that every frame displays the same portion of the xy-plane. This is relatively easy to do if we know approximately when the apple attains its largest xand y-coordinates: with_slider_draw(x, times, point_type = filled_circle, xrange = [0, 900], yrange = [0, 400], color = green, point_size = 2, points([[horizontal(9.8, 100, %pi/3, x), vertical(9.8, 100, %pi/3, x)]]), color = red, line_type = dots, parametric(horizontal(9.8, 100, %pi/3, t), vertical(9.8, 100, %pi/3, t), t, 0, x), color = black, label_alignment = left, label([printf(false, "t = ~5,1f seconds", x), 200, 50])); Unlike our earlier attempt, this second animation should produce a reasonable sense of the apple’s motion as it flies through the air. Fig. 3-5 shows six of its frames.

308

3. GRAPHICS AND VISUALIZATION

Fig. 3-5. Six frames in an animation illustrating the trajectory of an apple launched with an initial speed of v0 = 100 m/sec at an angle of θ = π/3 radians under the effects of gravity.

3.4. PROJECTILES IN MOTION

309

Having successfully illustrated how a projectile’s position varies with time, we can now turn to the question of how a projectile’s trajectory changes with the initial choice of angle θ. As before, we begin by drawing a single frame. For example, we can draw the entire trajectory of an apple launched at an initial speed of v0 = 100 m/sec when θ = π/3 radians (or 60◦ ) as the following parametric curve: wxdraw2d(color = dark_green, line_width = 3, parametric(horizontal(9.8, 100, %pi/3, t), vertical(9.8, 100, %pi/3, t), t, 0, duration(9.8, 100, %pi/3)), color = black, label_alignment = left, label(["theta = 60.0 degrees", 200, 50]));

Although this image seems adequate, it is rather plain and could use with a bit more color. Maxima will allow us to apply some shading, but only to an explicit graph. Thus, we must first convert the pair of parametric equations describing our trajectory into a single explicit equation. We do this by solving the horizontal position equation for t and substituting the result into the vertical position equation as in the following: solve(x = horizontal(g, v0, theta, t), t); x ] [t = cos (θ) v0

310

3. GRAPHICS AND VISUALIZATION

rhs(%[1]); x cos (θ) v0 vertical(g, v0, theta, %); sin (θ) x g x2 − 2 cos (θ) 2 cos (θ) v02 y(g, v0, theta, x) := ’’%; g x2 sin (θ) x − y (v0, θ, x) := 2 cos (θ) 2 cos (θ) v02 It will also be convenient to find the largest value of x that can be used with our newly-defined function y(): horizontal(g, v0, theta, duration(g, v0, theta)); 2 cos (θ) sin (θ) v0 2 g xmax(g, v0, theta) := ’’%; 2 cos (θ) sin (θ) v0 2 xmax(g, v0, theta) := g With these functions in hand, we can call on the explicit() graphic constructor to create a more colorful picture of our trajectory: wxdraw2d(line_width = 2, fill_color = light_green, filled_func = true, explicit(y(9.8, 100, %pi/3, x), x, 0, xmax(9.8, 100, %pi/3)), color = dark_green, filled_func = false, explicit(y(9.8, 100, %pi/3, x), x, 0, xmax(9.8, 100, %pi/3)), color = black, label_alignment = left, label(["theta = 60.0 degrees", 200, 50]));

3.4. PROJECTILES IN MOTION

311

Note that the fill_color and filled_func options control the shading. The first of these can be set to any of the colors listed in Table 3-4 (see page 260), while the latter can be set to either true or false. In particular, observe that our command above plots the same trajectory twice: once with filled_func set to true to create the light green shading and once with filled_func set to false to draw the darker outline. Furthermore, we draw the outline after the shading so that Maxima will render it on top of the shading, and not the other way around. For similar reasons, we plot the label indicating the value of theta last of all. We can convert the stationary image above into an animation by recasting our wxdraw2d() command into a with_slider_draw() command as follows: float(%pi/2); 1.570796326794897 angles : makelist(1.57*i/30, i, 0, 30) $ with_slider_draw(theta, angles, line_width = 2, fill_color = light_green, filled_func = true, explicit(y(9.8, 100, theta, x), x, 0, xmax(9.8, 100, theta)), color = dark_green, filled_func = false, explicit(y(9.8, 100, theta, x),

312

3. GRAPHICS AND VISUALIZATION

x, 0, xmax(9.8, 100, theta)), color = black, label_alignment = left, label([printf(false, "theta = ~5,1f degrees", float(theta*180/%pi)), 200, 50])); This should produce an animation with rather strange and dizzying motion effects. To fix these, we must once again adjust the values of xrange and yrange, noting that the largest x-coordinates occur halfway in our animation (when θ = 45◦ ), while the largest ycoordinates occurs towards the end of the animation (as θ approaches 90◦ ). with_slider_draw(theta, angles, line_width = 2, xrange = [0, 1000], yrange = [0, 550], fill_color = light_green, filled_func = true, explicit(y(9.8, 100, theta, x), x, 0, xmax(9.8, 100, theta)), color = dark_green, filled_func = false, explicit(y(9.8, 100, theta, x), x, 0, xmax(9.8, 100, theta)), color = black, label_alignment = left, label([printf(false, "theta = ~5,1f degrees", float(theta*180/%pi)), 200, 50])); Fig. 3-6 shows six of the frames in this new-and-improved animation. Observe how the height and breadth of the parabolic trajectories change with the angle θ. Maxima allows you to save your animations as animation files ending with the file extension .gif. This file type was originally developed by CompuServe in the 1980’s and is now supported by most web browsers. To save an animation, simply right-click its image in the iris and select the “Save Animation” option. A dialog window will open up and ask you to choose a name and location for your file. Once you have saved your animation, you can send it to a friend over email or put it on a website for a world wide web of followers to admire. Let the “oohs” and “aahs” begin!

3.4. PROJECTILES IN MOTION

Fig. 3-6. Six frames in an animation illustrating the trajectory of an apple launched at a variety of angles with an initial speed of v0 = 100 m/sec under the effects of gravity.

313

Exercises for Section 3.4 1. In this section, we explored the effects that two of the parameters in the equations for projectile motion had on the shape of a projectile’s trajectory. Consider the other two parameters in turn. (a) Create an animation that illustrates how a projectile’s trajectory varies with the initial velocity. You will want to choose a range of positive values for v0 , say from 5 m/sec to 100 m/sec. (b) Create an animation that illustrates how a projectile’s trajectory varies with the strength of gravity. You will want to choose a range of positive values for g, say from 1 m/sec2 to 20 m/sec2 . 2. Create an animation of a particle moving in uniform circular motion. Each frame of your animation should display the position and trajectory of a particle as it moves around a circle according to the parametric equations ⎧ ⎨x(t) = r cos(ω t) ⎩y(t) = r sin(ω t), where r > 0 and ω > 0 are two positive constants. Since this motion is periodic, you only need to make an animation of a single rotation and let it repeat over and over again. In that case, make sure that the end of one iteration of your animation blends in with the start of the next without jittering or stalling. 3. Create an animation of a particle moving left and right in simple harmonic motion according to the position function √ x(t) = cos( k t), where k > 0 is a positive constant. Since this motion is periodic, you only need to make an animation of a single oscillation and let it repeat over and over again. In that case, make sure that the end of one iteration of your animation blends in with the start of the next without jittering or stalling. 4. If a particle in simple harmonic motion is placed in some fluid, then its oscillations will experience some dampening. This dampened oscillation can be described by the position function √ x(t) = e−bt cos( k t),

EXERCISES

315

where k > 0 and b > 0 are two positive constants. Create an animation that illustrates this dampened oscillation. Since the position function is not periodic, your animation should display at least five oscillations. 5. The following implicit equation describes a surface known as a hyperboloid : x2 + y 2 − z 2 = d. Here, the constant d determines the overall shape of the surface. (a) Use Maxima’s drawing capabilities to verify empirically each of the following statements. ◦ If d > 0 then the surface consists of a single connected component. ◦ If d < 0 then the surface consists of two connected component. ◦ If d = 0 then the surface is the union of two cones. (b) Create an animation that shows a hyperboloid of one sheet transforming into a hyperboloid of two sheets by letting the value of d vary between 1 and −1.

3.5. Fourier Likes It Hot The problem of understanding the physics of heat has a long and varied history, but the great breakthrough came at the beginning of the nineteenth century with Jean Baptiste Joseph Fourier (1768 – 1830). Fourier was a French mathematician and physicist and his study of the problem of heat transfer culminated with the discovery of a partial differential equation known as the heat equation. Like the differential equations of Section 3.3, the heat equation describes how temperature changes with respect to time. However, since temperature can viewed as a function of both location and time, the heat equation involves partial derivatives. In particular, according to Fourier, temperature u(x, y, z, t) obeys the partial differential equation  2  ∂ u ∂2u ∂2u ∂u =α + + , ∂t ∂x2 ∂y 2 ∂z 2 where α is a constant, specific to the material through which heat is ∂ 2u ∂ 2u ∂ 2u flowing, known as thermal diffusivity , while ∂u ∂t , ∂x2 , ∂y 2 , and ∂z 2 give the first or second derivatives of the function u when only one variable is allowed to change. To see the heat equation in action, imagine heat flowing through a long metal rod. For the sake of simplicity, we will assume that the rod is made of a single material, that it has constant cross-section and uniform density, and that it is encased in a thermal coating, so that no heat can enter or leave the rod except at its ends. Each end of the rod is placed in direct contact with a heat source which maintains a constant temperature. The heat source on the left end will be kept at 100◦ C, while the one on the right will be kept at 50◦ C, as shown in Fig. 3-7. Suppose that the initial temperature throughout the rod is 0◦ C. As soon as heat begins to flow into the rod, this temperature will begin

100◦ C

50◦ C insulated metal rod

heat source

heat source

Fig. 3-7. Schematic of a simple heat flow experiment.

318

3. GRAPHICS AND VISUALIZATION

to increase. Since heat cannot escape from the sides of the rod, we can assume that the temperature throughout any cross-section of the rod will be constant. In other words, we can consider temperature as a function of only two variables (x and t). If we assume that the rod has a length of L, then we will initially have a discontinuous temperature of ⎧ ⎪ ⎪ ⎨100 if x = 0, u(x, 0) =

0 ⎪ ⎪ ⎩50

if 0 < x < L, if x = L.

When t > 0, the temperature function will be governed by the heat equation, which in this case simply becomes ∂u ∂2u =α . ∂t ∂x2 Furthermore, since the heat sources at each end maintain a constant temperature, the temperature function must also satisfy the boundary conditions u(0, t) = 100 and u(L, t) = 50 for all values of t. After enough time passes, the rod will reach a steady state at which point the temperature in the rod will depend only on position, varying linearly between the temperatures of the two heat sources: u(x, tsteady ) = 100 −

50 L

x.

Note that this function satisfies both the heat equation and the boundary conditions above. Our goal, then, is to find a function that is initially zero everywhere (except at the endpoints) and that slowly evolves to this steady state solution. One way to solve this problem is to numerically approximate all of the partial derivatives in the heat equation by finite differences. This is known as the finite difference method . In particular, we will partition the domain in space using m + 1 discrete points x0 , x1 , . . ., xm ; and in time using n+1 discrete points t0 , t1 , . . . tn . In both cases, we will assume a uniform partition so that the difference between two consecutive space points is Δx = xi − xi−1 = h and the difference between two consecutive time points is Δt = tj − tj−1 = k.

3.5. FOURIER LIKES IT HOT

319

Finally, we will denote the temperature at position xi and time tj as ui,j = u(xi , tj ). Before considering the derivatives in the heat equation, we note that there are several ways in which we might compute the derivative of an arbitrary function f . In the traditional definition, f  (a) is computed as the limit of a forward difference of the form f  (a) = lim

h→0

f (a + h) − f (a) , h

where we look forward to a future value of f (at least when we think of h as being positive). Of course, we can also compute f  (a) as a backward difference of the form f  (a) = lim

h→0

f (a) − f (a − h) , h

or even as a central difference of the form f  (a) = lim

h→0

f (a + h) − f (a − h) . 2h

You will observe that the backward difference is simply the result of replacing h in the forward difference with −h, and that the central difference is the average of the forward and backward differences. In all three cases, as h approaches zero, the difference quotients approach the same derivative. We now come back to the heat equation. We will first approximate the partial derivative with respect to time as the familiar forward difference ui,j+1 − ui,j u(xi , tj + k) − u(xi , tj ) ∂u (xi , tj ) ≈ = , ∂t k k which we might define in Maxima as: dudt : (u[i,j+1] - u[i,j])/k; ui,j+1 − ui,j k In contrast, we will approximate the second derivative with respect to

320

3. GRAPHICS AND VISUALIZATION

position as the central difference ∂u ∂u (xi + h, tj ) − (xi − h, tj ) ∂ u ∂x ∂x . (x , t ) ≈ i j ∂x2 2h 2

Finally, we will approximate the first derivatives in this expression using a backward difference and a forward difference, respectively, as ui+1,j − ui,j u(xi + h, tj ) − u(xi , tj ) ∂u (xi + h, tj ) ≈ = , ∂x h h ∂u ui,j − ui−1,j u(xi , tj ) − u(xi − h, tj ) (xi − h, tj ) ≈ = : ∂x h h dudxp : (u[i+1,j] - u[i,j])/h; ui+1,j − ui,j h dudxm : (u[i,j] - u[i-1,j])/h; ui,j − ui−1,j h Therefore, our second derivative becomes d2udx2 : ratsimp((dudxp - dudxm)/(2*h)); ui+1,j − 2ui,j + ui−1,j 2h2 The heat equation can then be rewritten in its discrete form as ui,j+1 =

  αk  αk  ui+1,j − ui,j + ui,j + 2 ui−1,j − ui,j 2 2h 2h

thanks to the following symbolic transformation: dudt = alpha * d2udx2; α (ui+1,j − 2ui,j + ui−1,j ) ui,j+1 − ui,j = k 2h2

3.5. FOURIER LIKES IT HOT

321

solve(%, u[i,j+1]); (αui+1,j − 2αui,j + αui−1,j ) k + 2h2 ui,j ] [ui,j+1 = 2h2 subst([u[i+1,j] = u[i,j] + A, u[i-1,j] = u[i,j] + B], %); (α (ui,j + B) + α (ui,j + A) − 2αui,j ) k + 2h2 ui,j ] [ui,j+1 = 2h2 expand(%); Bαk Aαk [ui,j+1 = + + ui,j ] 2h2 2h2 subst([A = u[i+1, j] - u[i,j], B = u[i-1, j] - u[i,j]], %); α (ui+1,j − ui,j ) k α (ui−1,j − ui,j ) k [ui,j+1 = + + ui,j ] 2h2 2h2 We can interpret this discrete version of the heat equation by noting that, in our partition of the rod, a point xi can only exchange heat with its two neighbors xi−1 and xi+1 . It is precisely this heat transfer that causes the temperature to change. In particular, the temperature change at xi in our equation consists of two terms, each of which is proportional to the difference in temperature between xi and one of its neighbors. When ui,j is smaller than one of these neighboring temperatures, the corresponding term will be positive, indicating an influx of heat and an increase in temperature. On the other hand, when ui,j is greater than one of its neighboring temperatures, the corresponding term will be negative indicating an outflow of heat and a decrease in temperature. One last technical comment bears mentioning: This finite difference method is known to be both numerically stable and convergent whenever αk ≤ 1. h2 As long as we stay within this bound, the numerical errors are proportional to the time step k and the square of the space step h2 . If we exceed this bound, however, each point on the rod will be taking in more energy than its neighbors can supply and the temperatures will blow up to infinity.

322

3. GRAPHICS AND VISUALIZATION

With all of the hard theoretical work behind us, we can now simulate our simple heat flow experiment with a function that takes as inputs the initial temperatures (Tleft, Tmid, and Tright), the thermal diffusivity constant (alpha), the length of the rod (L), the duration of the simulation (T), and the number of partition segments in space (m) and in time (n). The idea behind our function is to store the temperatures throughout the metal rod in a list oldt. Except for the endpoints, which will be set to the values of Tleft and Tright, all of these will start out with the value of Tmid. Next, our function will apply the discrete heat equation to determine the new temperatures after one time step, keeping the endpoints at the same temperature as their adjacent heat sources. We will store these new temperatures in the list newt. At the start of each iteration, newt will be contain a duplicate copy of the data in oldt, as produced by the function copylist(). Once all of the temperatures are adjusted to their new values, they are displayed by a printf() in an easy-to-read table. Then the old temperatures are replaced by the new ones and the entire process repeats once again. Of course, before any of this is done, our function should check that the stability condition above is satisfied. Our implementation of the finite difference method is given by the following definition: Fourier(Tleft, Tmid, Tright, alpha, L, T, m, n) := block([h, k, r, i, j, oldt, newt], h : L/m, k : T/n, r : alpha * k / h^2, /* test stability condition */ if r > 1 then return("Error: Solution is unstable!"), oldt : append([Tleft], makelist(Tmid, i, 1,m-1), [Tright]), printf(true," ~{ ~5,1f ~} ~%", oldt), /* loop iterates Finite Difference Method */

3.5. FOURIER LIKES IT HOT

323

for j : 1 thru n do (newt : copylist(oldt), for i : 2 thru m do (newt[i] : newt[i] + r/2 * (oldt[i+1] - oldt[i]), newt[i] : newt[i] + r/2 * (oldt[i-1] - oldt[i])), printf(true," ~{ ~5,1f ~} ~%", newt), oldt : newt)) $ To test our implementation, let us consider a 25 cm rod made out of gold, whose thermal diffusivity is 1.27 cm2 /sec. We will simulate the heat flow in this rod for a period of 20 seconds, using a partition of m = 10 by n = 10 points, by entering: Fourier(100, 0, 50, 1.27, 25, 20, 10, 10); 100.0 0.0 100.0 20.3

0.0 0.0

0.0 0.0

0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0

0.0 0.0 50.0 0.0 10.2 50.0

100.0

32.4

4.1

0.0

0.0

0.0

0.0

0.0

2.1

16.2

50.0

100.0

40.4

9.0

0.8

0.0

0.0

0.0

0.4

4.5

20.2

50.0

100.0 100.0

46.1 50.5

13.7 18.0

2.3 4.2

0.2 0.6

0.0 0.1

0.1 0.3

1.2 2.1

6.9 9.0

23.1 25.2

50.0 50.0

100.0 100.0

53.9 56.8

21.8 25.2

6.3 8.4

1.2 2.0

0.2 0.5

0.6 1.0

3.1 4.2

10.9 12.6

27.0 28.4

50.0 50.0

100.0 100.0

59.1 61.2

28.2 30.9

10.5 12.6

3.0 4.1

0.9 1.5

1.6 2.2

5.3 6.3

14.1 15.4

29.6 30.6

50.0 50.0

100.0 done

62.9

33.3

14.6

5.3

2.2

2.9

7.3

16.7

31.4

50.0

After 20 seconds, the temperature throughout the rod has certainly increased from its initial condition, but it still has a long way to go before it reaches steady state. Unfortunately, even nicely aligned tables like the one above can be difficult to read. To better see the effects of heat flow in our metal rod, we will construct a graphical version of this function. Since each row of the table describes the distribution of temperatures in the rod at a given time, it seems natural to create an animation that shows

324

3. GRAPHICS AND VISUALIZATION

how this distribution evolves from one time step to the next. We do this in the following modification of Fourier(), which replaces every instance of the printf() command with an instruction to save the temperature data needed to create the desired animation. In particular, we will assign the local variable frames to be a two-tiered list of location-temperature ordered pairs. Each sublist of frames will contain the entire distribution of temperatures corresponding to a given frame of the animation. Since these sublists will be added using frontconcatenation, newer frames will appear at the beginning of the list, a fact that we will need to keep in mind at the final stage of our implementation, when we extract the data from frames and render our animation. Our modified function definition looks like this: Fourierplot(Tleft, Tmid, Tright, alpha, L, T, m, n) := block([h, k, r, i, j, oldt, newt, frames], h : L/m, k : T/n, r : alpha * k / h^2, /* test stability condition */ if r > 1 then return("Error: Solution is unstable!"), oldt : append([Tleft], makelist(Tmid, i, 1, m-1), [Tright]), frames : [makelist([(i-1)*h, oldt[i]], i, 1, m+1)], /* loop iterates Finite Difference Method */ for j : 1 thru n do (newt : copylist(oldt), for i : 2 thru m do (newt[i] : newt[i] + r/2 * (oldt[i+1] - oldt[i]), newt[i] : newt[i] + r/2 * (oldt[i-1] - oldt[i])), frames : cons(makelist([(i-1)*h, newt[i]], i, 1, m+1), frames),

3.5. FOURIER LIKES IT HOT

325

oldt : newt), /* create an animation from the data stored in frames */ with_slider_draw(j, makelist(i, i, 0, n), xlabel = "position on rod", xrange = [0, L], ylabel = "temperature", yrange = [-5, 100], color = red, point_type = dot, points_joined = true, points(frames[n+1-j]), color = black, label_alignment = left, label([printf(false, "t = ~5,1f seconds", j*k), 1, 10]))) $ We can run a similar simulation as above, but for a duration of T = 350 seconds with m = 15 space and n = 350 time partitions by entering: wxanimate_framerate : 10 $ Fourierplot(100, 0, 50, 1.27, 25, 350, 15, 350); Fig. 3-8 shows six frames in the resulting animation. Observe that the first frame shows a discretized version of our initial discontinuous temperatures. In short order, as the internal temperatures increase, a seemingly smooth, concave up curve emerges. Slowly but surely, the local minimum in this curve will make its way to the right hand side of the graph. The final frame shows a graph that is very nearly linear. Perhaps you can still detect a minuscule amount of concavity in the last graph. With a longer duration, this, too, will be reduced until the temperature of the rod is, for all intents and purposes, in the predicted linear steady state.

326

3. GRAPHICS AND VISUALIZATION

Fig. 3-8. Six frames in an animation illustrating the changing temperatures inside a metal rod.

Exercises for Section 3.5 1. Consider the function f (x) = sin(x). (a) On the same graph, plot two full oscillations of the derivative f  (x) and the forward difference approximation with h = 0.5: fforward (x) =

f (x + h) − f (x) . h

How are the two graphs different? What features do they have in common? (b) On the same graph, plot two full oscillations of the derivative f  (x) and the backward difference approximation with h = 0.5: fbackward (x) =

f (x) − f (x − h) . h

How are the two graphs different? What features do they have in common? (c) On the same graph, plot two full oscillations of the derivative f  (x) and the central difference approximation with h = 0.5: fcentral (x) =

f (x + h) − f (x − h) . 2h

How are the two graphs different? What features do they have in common? 2.

(a) Run a similar simulation to the one described in this section, but using a 25 cm rod made of silver, whose thermal diffusivity is 1.656 cm2 /sec. Approximately how long does it take before the temperature appears to be in a steady state? (b) Repeat the simulation using a 25 cm rod made of aluminum, whose thermal diffusivity is 0.852 cm2 /sec. Approximately how long does it take before the temperature appears to be in a steady state? (c) Use your data from parts (a) and (b) to determine the apparent relationship between the time it takes a metal rod to reach steady state and its thermal diffusivity. Is it a direct relationship or an inverse relationship? Explain.

3. In our implementation of Fourier(), the inputs L, T , m, and n were selected independently of one another. Instead, suppose that we first select L, m, and n, and that based on those values,

328

3. GRAPHICS AND VISUALIZATION

we set h=

L m

and

k=

h2 . α

(a) In this scenario, what is the value of αh2k ? Is the finite difference method stable? What does the discrete heat equation say to do to the temperatures at each iterative step? (b) How does the value of T depend on m and n? Does T increase or decrease when the number of space partition points is increased? How about when the number of time partition points is increased? (c) Implement this version of the finite difference method as a Maxima function. 4. Consider the impact that the finite difference method’s stability condition has on the size of the partitions used in our implementation of Fourier’s heat equation. (a) Suppose that we select a partition in time with Δt = k = 1. How does the stability condition limit our choice of partition in space? Can m be chosen arbitrarily small or arbitrarily large? Explain. (b) Suppose that we select a partition in space with Δx = h = 1. How does the stability condition limit our choice of partition in time? Can n be chosen arbitrarily small or arbitrarily large? Explain. 5. Our definition of Fourier() in this section contains an “escape hatch” which prevents a simulation from running if αk > 1. h2 Remove this escape hatch and run a simulation of the 25 cm gold rod under this unstable condition. What happens to temperature of the rod according to your simulation? Where does the temperature appear to grow the fastest? Explain why this behavior is unrealistic.

3.6. Plotting Curves by Sampling Throughout this chapter, we have explored Maxima’s ability to graph several types of equations by means graphic constructors like explicit(), implicit(), parametric(), and polar(). All of these graphing constructors work by plotting a finite number of sample points and then joining these with straight line segments. The overarching philosophy is that we can create the illusion of curvature by plotting a large number of sample points. Therefore, the question before us is exactly how to select a sufficiently large set of sample points. In the case of implicit plots, the algorithm used to find sample points is quite elaborate, so we shall not describe it here. In contrast, the sampling algorithm used to render explicit, parametric, and polar graphs can be implemented as a fairly simple iterative process. Let us consider the particular case of a pair of parametric equations, written in the form of a vector-valued function as   f(t) = x(t), y(t) , that we wish to plot over a given domain a ≤ t ≤ b. The simplest approach for finding a set of sample points for this type of graph is known as uniform sampling . This involves partitioning the domain into n equal pieces of size Δt =

b−a . n

The parametric function is then evaluated at the input values tj = a + jΔt, as illustrated by Fig. 3-9. This gives a list of n + 1 sample points which can be used to render a piecewise linear approximation of the curve in question. Note that the sample points are chosen so they are equidistant in the domain of f , but the distances between them can vary significantly in the curve itself. The definition below implements this uniform sampling strategy for a parametric function whose coordinates are given as formulas x and y in terms of a free variable, whose name will be extracted by the function listofvars() and stored as the local variable t. To avoid any difficulties that might result if one or the other coordinate function is a constant, we apply this function to the triple [x, y, t]; this guarantees that at least one variable name is produced. The sampling

330

3. GRAPHICS AND VISUALIZATION

t0

f(t0 )

t1

f(t1 )

t2

···

tn−2 tn−1 tn

f(tn ) f(t2 ) f(tn−2 )

f(tn−1 )

Fig. 3-9. Plotting a curve using uniform sampling. process is controlled by a single for loop that considers the values of tj in turn and accumulates the resulting sample points into the list pts using the front-concatenation function cons(). This means that the sample points are placed in reverse order in the list but, as we shall see, that will be inconsequential when we go ahead and graph them. Finally, note that as a precautionary measure, we simplify the coordinates of our sample points using the radcan() function. This will avoid some annoying roundoff errors that can arise when the sample points are plotted. unifsample(x, y, a, b, n) := block([t, deltat, tj, pts, j], t : listofvars([x, y, t])[1], deltat : (b - a)/n, tj : a, pts : [ ], /* partition the interval [a, b] into n equal pieces */ for j : 0 thru n do (pts : cons(radcan(subst(t = tj, [x, y])), pts), tj : tj + deltat), return(pts)) $

3.6. PLOTTING CURVES BY SAMPLING

331

In order to understand the properties of the uniform sampling strategy, we shall consider three different parameterizations of the unit circle, plotting each one as a list of sample points with the following default options: load(draw) $ set_draw_defaults(proportional_axes = xy, point_type = dot, points_joined = true, xrange = [-1.25, 1.25], yrange = [-1.25, 1.25]) $ Our first parametrization comes directly from trigonometry and corresponds to wrapping a line segment of length 2π counterclockwise around the circumference of the circle: where 0 ≤ θ ≤ 2π. f1 (θ) = (cos(θ), sin(θ)) , Note that our uniform sampling algorithm has no problem detecting that in this case the parameter name is theta. Indeed, Maxima can readily produce piecewise-linear pictures of the unit circle when we call our sampling function unifsample() from within the drawing command wxdraw2d(), using the graphic constructor points() to render the resulting sample points as follows: wxdraw2d(points(unifsample(cos(theta), sin(theta), 0, 2*%pi, 10)));

332

3. GRAPHICS AND VISUALIZATION

Sampling the domain more often improves the resolution of the graph. For instance, a choice of n = 50 produces a curve that appears to be perfectly smooth: wxdraw2d(points(unifsample(cos(theta), sin(theta), 0, 2*%pi, 50)));

The second parameterization that we shall consider is obtained by letting the x-coordinate follow the parabolic arc x = w2 − 1, so that its values start at 1, go down to −1, and then come back up to 1. The value of the y-coordinate is then adjusted accordingly: & '  √ √ f2 (w) = w2 − 1, w 2 − w2 , where − 2 ≤ w ≤ 2. Observe that this function traces its way around the circle in a clockwise fashion, mapping negative values of w to the lower half of the circle and positive values of w√to the upper half of the circle, and sending both endpoints w = ± 2 to the single point (1, 0). We can plot this parameterization with the following commands:

3.6. PLOTTING CURVES BY SAMPLING

333

wxdraw2d(points(unifsample(w^2 - 1, w * sqrt(2 - w^2), -sqrt(2), sqrt(2), 10)));

wxdraw2d(points(unifsample(w^2 - 1, w * sqrt(2 - w^2), -sqrt(2), sqrt(2), 250)));

Finally, our third parametrization corresponds to wrapping an infinite line around a circle by means of “stereographic projection”:   2t 1 − t2  , , where − ∞ < t < ∞. f3 (t) = 1 + t2 1 + t 2 This function places the line’s origin (t = 0) at the north pole of the circle, while the infinite ends of the line are mapped to points closer

334

3. GRAPHICS AND VISUALIZATION

and closer to, and on either side of, the south pole. Since we cannot sample over an infinite domain, in the examples below we shall limit ourselves to the interval −25 ≤ t ≤ 25, with the understanding that we can always plot a larger portion of the circle by sampling over a larger domain: wxdraw2d(points(unifsample(2 * t/(1 + t^2), (1 - t^2)/(1 + t^2), -25, 25, 10)));

wxdraw2d(points(unifsample(2 * t/(1 + t^2), (1 - t^2)/(1 + t^2), -25, 25, 500)));

3.6. PLOTTING CURVES BY SAMPLING

335

All of the plots above illustrate how, by increasing the number of points plotted, we can improve the quality of the resulting picture and thus create a sense of curvature. How many sample points are required to achieve this effect, however, depends on the given parameterization. For instance, the functions above required anywhere from n = 50 to n = 500 sample points to convincingly render a smooth circular arc. By changing our default plotting options so that our sample points are drawn more visibly, we can readily observe that only the first parameterization spaces out its sample points evenly along the circle: set_draw_defaults(proportional_axes = xy, point_type = filled_circle, xrange = [-1.25, 1.25], yrange = [-1.25, 1.25]) $ wxdraw2d(points(unifsample(cos(theta), sin(theta), 0, 2*%pi, 50)));

336

3. GRAPHICS AND VISUALIZATION

wxdraw2d(points(unifsample(w^2 - 1, w * sqrt(2 - w^2), -sqrt(2), sqrt(2), 250)));

wxdraw2d(points(unifsample(2 * t/(1 + t^2), (1 - t^2)/(1 + t^2), -25, 25, 500)));

This means that even when the number of sample points n is sufficiently large to make one portion of the curve appear smooth, other portions mights still appear jagged. One way in which we can solve this problem is to pick a much larger value of n to smooth out the sparsest portions of the curve, but this is inevitably wasteful on the denser portions of the curve.

3.6. PLOTTING CURVES BY SAMPLING

337

A more elaborate solution to this problem is to employ what is commonly known as adaptive sampling . This means that the sections of the domain where points are mapped more sparsely are partitioned more aggressively than other portions of the domain where points are mapped closer to one another. As with uniform sampling, we start by dividing the domain into some number of equal segments and sampling the parametric function at their endpoints. We then measure the distance between consecutive sample points using the following function: distance(P, Q) := sqrt((P[1]-Q[1])^2 + (P[2]-Q[2])^2) $ Whenever this distance becomes larger than some given tolerance, the sampling will be refined so that extra sample points can be added to fill in the gaps. This will ensure that consecutive sample points are never too far apart on the curve. The following definition implements a simple version of adaptive sampling. In addition to the inputs used in the uniform sampling algorithm, this function will also take values for a tolerance (tolerance) and a maximum refinement depth (maxdepth). If a pair of consecutive sample points is father apart than the tolerance, a recursive refinement is performed and some additional intermediate points will be added in between them. This process will then repeat itself until either (a) the sample points are closer than the given tolerance, or (b) the sampling reaches the given maximum depth for refinements. The local variables oldpt and newpt hold the coordinates of the two most recent sample points visited and assist in determining whether or not a refinement is necessary. If it is, then the collection of intermediate sample points produced by the refinement are appended to the front of the list rest(pts), with the result that the duplicate endpoints are eliminated; otherwise a single new sample point is added to pts with the concatenation function cons(). The process then repeats to consider the next pair of sample points. adaptsample(x, y, a, b, n, tolerance, maxdepth) := block([t, deltat, tj, pts, j, oldpt, newpt], /* partition the interval [a, b] into n equal pieces partition subintervals larger than tol recursively */ t : listofvars([x, y, t])[1],

338

3. GRAPHICS AND VISUALIZATION

deltat : (b - a)/n, tj : a, oldpt : radcan(subst(t = tj, [x, y])), pts : [oldpt], for j : 1 thru n do (newpt : radcan(subst(t = tj + deltat, [x, y])), if distance(oldpt, newpt) > tolerance and maxdepth > 1 then pts : append(adaptsample(x, y, tj, tj + deltat, n, tolerance, maxdepth - 1), rest(pts)) else pts : cons(newpt, pts), tj : tj + deltat, oldpt : newpt), return(pts)) $ In an example like our trigonometric parameterization of the circle, where the uniformly sampled points are already spread out evenly along the curve, there is no visible difference between adaptive and uniform sampling. For instance, when n = 5, tolerance = 0.15, and maxdepth = 1, we get five equally spaced sample points. Increasing maxdepth to 2 or 3 allows for one or two additional levels of refinement, resulting in 25 or 125 equally spaced points, respectively.3 By this stage, every pair of consecutive sample points is within the given tolerance of 0.15 so increasing the maximum depth any further will not cause any more refinements or yield any additional sample points. Observe that, for this parameterization, our adaptive sampling algorithm produces precisely the same collection of points as our earlier implementation of uniform sampling:

3 This count ignores the last point, which is a duplicate of the first.

3.6. PLOTTING CURVES BY SAMPLING

wxdraw2d(points(adaptsample(cos(theta), sin(theta), 0, 2*%pi, 5, 0.15, 1)));

wxdraw2d(points(adaptsample(cos(theta), sin(theta), 0, 2*%pi, 5, 0.15, 2)));

339

340

3. GRAPHICS AND VISUALIZATION

wxdraw2d(points(adaptsample(cos(theta), sin(theta), 0, 2*%pi, 5, 0.15, 3)));

length(adaptsample(cos(theta), sin(theta), 0, 2*%pi, 5, 0.15, 3)); 126 In contrast, the adaptive sampling strategy can make a remarkable difference when uniformly sampled points are not spread out evenly along a curve. For example, in the case of our second parameterization, adaptive sampling will add more points on the right side of the circle, where the uniform sampling is relatively sparse; the result is that the entire circle can be adequately sampled to a tolerance of 0.15 with only 142 points, roughly half of the number required by our uniform sampling strategy. To accomplish this task, our algorithm performs as many as five levels of refinement, but only in the sparsest regions of the curve, where they are most needed.

3.6. PLOTTING CURVES BY SAMPLING

341

wxdraw2d(points(unifsample(w^2 - 1, w * sqrt(2 - w^2), -sqrt(2), sqrt(2), 250)));

wxdraw2d(points(adaptsample(w^2 - 1, w * sqrt(2 - w^2), -sqrt(2), sqrt(2), 5, 0.15, 5)));

length(adaptsample(w^2 - 1, w * sqrt(2 - w^2), -sqrt(2), sqrt(2), 5, 0.15, 5)); 142 In a similar way, when applied to our stereographic projection parameterization, adaptive sampling will add more points on the upper

342

3. GRAPHICS AND VISUALIZATION

half of the circle (where the parametrization was sparser) and fewer on the bottom half (where it was denser). In fact, we can even see some small gaps open up in the bottom half of the circle, where the uniform sampling was particularly wasteful. In this case, only 110 points are required to sample the portion of the circle corresponding to −25 ≤ t ≤ 25 with a tolerance of 0.15. Notice that this is about a fifth of what was needed by uniform sampling! wxdraw2d(points(unifsample(2 * t/(1 + t^2), (1 - t^2)/(1 + t^2), -25, 25, 500)));

wxdraw2d(points(adaptsample(2 * t/(1 + t^2), (1 - t^2)/(1 + t^2), -25, 25, 5, 0.15, 5)));

3.6. PLOTTING CURVES BY SAMPLING

343

length(adaptsample(2 * t/(1 + t^2), (1 - t^2)/(1 + t^2), -25, 25, 5, 0.15, 5)); 110 Although we have described the uniform and adaptive sampling algorithms only in terms of parametric functions, it should come as no surprise that they can both be modified to create explicit and polar graphs as well (for instance, see Exercises 1 and 2). In fact, as you may have already guessed, Maxima’s parametric() and polar() graphic constructors implement a uniform sampling algorithm in which the number of sample points used is controlled by the option nticks, which defaults to 29. On the other hand, the explicit() graphic constructor uses an adaptive sampling algorithm in which nticks (again, with a default value of 29) gives the initial number of points used before any refinements occur and adapt_depth (with a default value of 10) gives the maximum number of refinements made.

Exercises for Section 3.6 1. Modify the uniform and adaptive sampling functions described in this section so that they can be used to graph an explicit function f (x). One way to do this is to exploit the natural parametrization ⎧ ⎨x(t) = t ⎩y(t) = f (t). Your functions should take a single formula for f (x) in terms of a free variable, along with bounds for the variable, the number of sample points to be produced, and in the case of adaptive sampling, the tolerance and maximum depth of sampling to be used. 2. Modify the uniform and adaptive sampling functions described in this section so that they can be used to graph a polar function r(θ). One way to do this is to exploit the change-of-coordinate formulas ⎧ ⎨x(θ) = r(θ) · cos(θ) ⎩y(θ) = r(θ) · sin(θ). Your functions should take a single formula for r(θ) in terms of a free variable, along with bounds for the variable, the number of sample points to be produced, and in the case of adaptive sampling, the tolerance and maximum depth of sampling to be used. 3. Consider the curve described by the parametric equations ⎧ ⎪ et + e−t ⎪ ⎪ ⎪ ⎨x(t) = cosh(t) = 2 ⎪ et − e−t ⎪ ⎪ . ⎪ ⎩y(t) = sinh(t) = 2 The functions sinh(t) and cosh(t) used above are known as the hyperbolic sine and cosine, respectively. (a) Plot the curve described by this parameterization. Where would a uniformly-partitioned set of sample points be spread out the most? Where would they be clustered together the most? (b) What kind of curve does this parameterization describe? Find an implicit (nonparametric) equation for this curve.

EXERCISES

345

Hint: Write the parametric equations above in terms of A = et and B = e−t , and ask Maxima to simultaneously solve for A and B in terms of x and y. Then use the fact that A · B = 1 to find an implicit equation in terms of x and y. 4. The following polar equation describes a curve known as a conic section: r(θ) =

1 . 1 − ε cos(θ)

The constant ε, which is known as the eccentricity, determines the overall shape of the curve as follows: ◦ If ε = 0 then the curve is a circle. ◦ If 0 < ε < 1 then the curve is an ellipse. ◦ If ε = 1 then the curve is a parabola. ◦ If ε > 1 then the curve is a hyperbola. For each of these cases, select an appropriate choice of ε and determine where on the conic section is a uniformly-partitioned set of sample points spread out the most, and where is it clustered together the most. 5. One way to detect where a parametric curve might require more sample points is to look at the derivative of the arc length, namely    2 2 ds dx dy = + . dt dt dt Presumably, a uniformly-sampled set of points will be spread out more wherever this derivative is large, and they will be clustered together wherever it is small. Examine the derivative of arc length for each of the three parameterizations of the unit circle given in this section; if you wish, you may use the primitive Maxima function diff(formula, variable) to compute the derivatives of the coordinate functions. Does this conjecture appear to hold for these parameterizations?

3.7. Plotting Lines with Pixels On multiple occasions throughout this chapter, we have relied on Maxima’s ability to draw line segments. Of course, when we say that Maxima “draws” a line segment, or any other graphical object for that matter, what we really mean is that it selects a certain portion of the computer screen and assigns it a specific intensity or color to set it apart from its surroundings. In this section, we will investigate how Maxima accomplishes this. The smallest unit of picture that can be represented on a computer screen is known as pixel (short for “picture element”). You can think of a pixel as a tiny point of colored light. Pixels are normally arranged on the screen in a two-dimensional rectangular grid known as a raster . For instance, a typical fifteen-inch laptop screen consists of a 1280 × 854 raster of pixels. It is common to describe a single pixel by two whole numbers giving its horizontal and vertical position. Furthermore, the color of each pixel on the computer screen is represented by three numbers giving the intensities of the “primary light colors” red, green, and blue.4 Thus, the act of “drawing” is more accurately described as rendering , that is, choosing which pixels get assigned to which color. As a simple example of how graphics are rendered on the computer screen, let us consider a function that mimics the coloring of a single pixel at position (x, y) of the raster. We will make use of the rectangle() graphic constructor to draw a small square filled with the color specified by the integers r, g, and b, each in the range between 0 and 255. Each of these integers is coded into a two-digit hexadecimal number (with leading zeroes, if needed) by the printf() directive ~2,’0X, as follows: rgbcolor(r, g, b) := printf(false, "#~2,’0X~2,’0X~2,’0X", r, g, b)$ For instance, the triple of integers r = 22, g = 137, and b = 250 is encoded as the hexadecimal code rgbcolor(22, 137, 250); #1689F A 4 In principle, the same is true for printed material, in which case the pixels are tiny dots of ink. However, in that setting, four primary colors are used: cyan, magenta, yellow, and black.

348

3. GRAPHICS AND VISUALIZATION

Our pixel() function will assign a code of this type to the option fill_color, and then draw a rectangle with opposite corners located at the points (x − 12 , y − 12 ) and (x + 12 , y + 12 ) as follows: pixel(x, y, r, g, b) := [fill_color = rgbcolor(r, g, b), rectangle([x-1/2, y-1/2], [x+1/2, y+1/2])] $ We can then use this function as if it were a graphic constructor on its own right by including it inside a wxdraw2d() command. For instance, as the command below illustrates, when all three intensities are set to the same value, the result is a pixel colored in some shade of gray, ranging from black when r = g = b = 0 (indicating no light whatsoever of any color) to white when r = g = b = 255 (indicating maximum light in all three colors). Similarly, maximizing one of the three intensities and setting the other two to zero produces a red, green, or blue pixel; while zeroing out one of the intensities and maximizing the other two produces a cyan, magenta, or yellow pixel: wxdraw2d(proportional_axes = xy, color = black, xrange = [0, 7], yrange = [0, 4], pixel(2, 3, 0, 0, 0), pixel(4, 3, 128, 128, 128), pixel(6, 3, 255, 255, 255), pixel(1, 2, 255, 0, 0), pixel(3, 2, 0, 255, 0), pixel(5, 2, 0, 0, 255), pixel(2, 1, 0, 255, 255), pixel(4, 1, 255, 0, 255), pixel(6, 1, 255, 255, 0));

3.7. PLOTTING LINES WITH PIXELS

349

To render a more complicated graphical element, Maxima needs to make several choices. For instance, if we want to render the graph of a surface based on some grid of sample points, Maxima first has to decide which pixel will correspond to each sample point. In other words, it needs to take the three spatial coordinates (x, y, z) for each sample point, which are presumably floating-point numbers, and convert them into two integral coordinates on the screen’s raster of pixels. Maxima will also need to determine the color that the surrounding pixels are to be given; this will depend on the placement of some imaginary light source, the position of the viewer, and the reflective properties of the surface. Both of these are challenging problems in projective geometry. Let us suppose that these difficult issues have been resolved, and that Maxima is now ready to connect the pixels representing two sample points with a straight line segment. This will involve transforming a continuous line segment into a discrete collection of pixels. The algorithm typically used to solve this discretization problem was developed in 1962 by Jack Elton Bresenham (b. 1937) at IBM’s development laboratory in San Jose, California. It uses only integer addition, subtraction and bit shifting, all of which are very fast operations in standard computer architectures. The basic idea behind Bresenham’s algorithm is to apply Euler’s method to the differential equation dy = m, dx where m is the slope of the line segment in question. However, instead of seeking an exact solution, the task now is to round the solution values into integer values, forming of the coordinates of the pixels to be rendered. The following definition presents very simple and inefficient implementation of this idea. For the sake of simplicity, we will assume that the line segment starts at the pixel (0, 0) and continues on to pixel (xfinal, yfinal) with a slope of m between zero and one. In this case, we can take horizontal steps of Δx = 1 and vertical steps of Δy = m. In particular, our rendered line segment will contain precisely one pixel per column, so that each pixel will be at most one row above or below its neighboring pixels. Note the use of the round() function, which ensures that the pixels’ y-coordinates are always integers.

350

3. GRAPHICS AND VISUALIZATION

Bresenham1(xfinal, yfinal, r, g, b) := block([m, x, y, pixels], m : yfinal/xfinal, y : 0, pixels : pixel(0, 0, r, g, b), /* loop repeats Euler’s Method and stores pixels */ for x : 1 thru xfinal do (y : y + m, pixels : append(pixel(x, round(y), r, g, b), pixels)), /* render stored pixels */ wxdraw2d(proportional_axes = xy, color = black, pixels)) $ Bresenham1(20, 7, 22, 137, 250);

There are two changes that we can make in order to significantly improve the efficiency of our line-rendering function above. The first of these involves storing the integral and fractional parts of y separately as two variables, yint and yfrac. This will help us eliminate the use of the round() function, which typically involves a costly change of binary encoding from floating-point numbers to integers. Initially, both yint and yfrac will be set to zero. Then, at each stage of Euler’s method, our procedure will attempt to increase yfrac by Δy = m, leaving yint untouched and placing the new pixel in the same row as

3.7. PLOTTING LINES WITH PIXELS

351

its predecessor. However, if yfrac + m ≥ 0.5 then our procedure will instead increase yint by one and yfrac by m − 1; this will have the effect of moving our new pixel up one row higher than its predecessor. In either case, the net value of yint + yfrac increases by the same amount Δy = m, so Euler’s method can proceed as usual. With this modification in place, the second version of our line-rendering function looks like this: Bresenham2(xfinal, yfinal, r, g, b) := block([m, x, yint, yfrac, pixels], m : yfinal/xfinal, yint : 0, yfrac : 0, pixels : pixel(0, 0, r, g, b), /* loop repeats Euler’s Method and stores pixels */ for x : 1 thru xfinal do (if yfrac + m < 0.5 then yfrac : yfrac + m else (yint : yint + 1, yfrac : yfrac + m - 1), pixels : append(pixel(x, yint, r, g, b), pixels)), /* render stored pixels */ wxdraw2d(proportional_axes = xy, color = black, pixels)) $ The second improvement that we will make to our line-rendering algorithm is to replace the role played by the fraction yfrac with an integer. More specifically, we note that at any given point in the computation, yfrac consists of a rational number with a denominator of xfinal. Since the denominator never changes, all we need to keep track of is the numerator. To make things even better, note that the outcome of the conditional statement in our implementation above

352

3. GRAPHICS AND VISUALIZATION

was determined by the predicate yfrac + m < 0.5, or equivalently, yfrac +

1 yfinal < . xfinal 2

After multiplying both sides by 2 · xfinal and clearing one side of the inequality, this becomes 2 · xfinal · yfrac + 2 · yfinal − xfinal < 0. Therefore, we shall let q represent the value of the “error function” on the left hand side of this expression. At each stage in our earlier implementation, yfrac would increase by either m or m − 1 (depending on the outcome of the conditional), so now q will increase by ⎧ ⎨2 · xfinal · m = 2 · yfinal if q < 0, Δq =   ⎩2 · xfinal · (m − 1) = 2 yfinal − xfinal if q ≥ 0. Thus, our final implementation of Bresenham’s algorithm, in which we replace the variable yint with the original variable name y, is given by the following definition: Bresenham3(xfinal, yfinal, r, g, b) := block([x, y, q, deltaq1, deltaq2, pixels], y : 0, pixels : pixel(0, 0, r, g, b), /* error function q determines when to increase y */ q : 2 * yfinal - xfinal, deltaq1 : 2 * yfinal, deltaq2 : 2 * (yfinal - xfinal), /* loop updates error function and stores pixels */ for x : 1 thru xfinal do (if q < 0 then q : q + deltaq1 else

3.7. PLOTTING LINES WITH PIXELS

353

(y : y + 1, q : q + deltaq2), pixels : append(pixel(x, y, r, g, b), pixels)), /* render stored pixels */ wxdraw2d(proportional_axes = xy, color = black, pixels)) $ The three procedures presented here will render a given line segment in exactly the same way, that is to say, selecting the same collection of pixels. However, you will note that our implementation of Bresenham3() uses only integer arithmetic, specifically integer addition and multiplication. Better still, since we only multiply times two, we can implement all three of the multiplications at the start of the procedure by shifting the bits in the binary codes for those numbers one place to the left. For instance, if we were to start with yfinal = 7 = 22 + 21 + 20 then we would compute 2 * yfinal = 14 = 23 + 22 + 21 by left-shifting the corresponding binary code as follows: 7



⇓ 14

000111 ⇓



001110.

This means that Bresenham’s algorithm can be run very quickly and efficiently at the level of “machine language,” that is, at the lowest level of binary programming. In fact, the machine language implementation is so fast that a computer can usually render a line with a changing endpoint as it tracks the motion of a user-controlled mouse, all without even hinting at the many computations that make this possible.

Exercises for Section 3.7 1. The functions described in this section only work for line segments in the region of the plane where 0 ≤ y ≤ x. This is the first of eight octants, numbered in counterclockwise order around the origin, that together make up the xy-plane. (a) Design a function octant() that takes a pair of coordinates (x, y) as input and returns the number of the octant containing the corresponding point. (b) All eight octants are congruent to one another by a sequence of rotations and reflections. For instance, points in the first and second octants are related to one another by the reflection (x, y) ↔ (y, x). Design a function that takes as input a pair of coordinates for a pixel in the first octant, an rgb-triple describing a color, and an octant number, and returns a function call to pixel() for the corresponding pixel in the selected octant. 2. Modify the Bresenham line-rendering algorithm so it works in all octants. You might want to perform the appropriate computations for a line segment in the first octant, and then use the reflections and rotations described in Exercise 1 to render the corresponding line segment in the correct octant. 3. The midpoint circle algorithm is a variant of Bresenham’s line algorithm used to render circles. For the sake of simplicity, let us assume that the center of the circle is at the origin and that the radius r is an integer number of pixels. The idea is to start at the pixel (r, 0) and to complete the first octant of the circle by selecting pixels (x, y) in a counterclockwise fashion until x < y. For the most part, the new pixel will be chosen directly above the previous one, at (x, y + 1). Occasionally however, we also move one step to the left, placing the new pixel at (x − 1, y + 1). This is happens when the value of error function q = r2 + r − x2 − (y + 1)2 becomes zero or negative. The following pseudocode describes an algorithm that renders a circular arc, keeping the value of q up to date even as x and y are modified by simply performing additions: circlearc(r): x Initially set x = r, y = 0, q = r, Δq1 = 1, and Δq2 = 2r + 1. y Place a pixel at location (x, y).

EXERCISES

355

z Increase y by 1, decrease Δq1 by 2, and increase q by Δq1 . { If q ≤ 0 or x = y then: decrease x by 1, decrease Δq2 by 2, and increase q by Δq2 . | Repeat steps y through { as long as x ≥ y. (a) Implement the algorithm above as a function in Maxima. (b) Make the necessary modifications to the function in part (a) so it fills in the remaining octants of the circle. You might want to do this by making use of the reflections and rotations described in Exercise 1. Your function should produce an entire circle like the one below: circle(7);

4.

(a) Build the following color wheel by using the triangle() graphic constructor and the rgbcolor() function. For the six labeled colors, use the rgbcolor() intensities given in this section. For the remaining colors, use the average of the two neighboring colors.

356

3. GRAPHICS AND VISUALIZATION

(b) Create three functions (r(i,n), g(i,n), and b(i,n)) that determine the color intensity of the ith triangle in a color wheel with n colors. For instance, you might define the red intensity function as follows: ⎧ ⎪ 0 if 0 ≤ i ≤ n3 , ⎪ ⎪ ⎪ ⎪ ⎪ ⎨round((i − n ) · 6 · 255) if n ≤ i ≤ n , 3 n 3 2 r(i,n) = n 5n ⎪ ≤ i ≤ , 255 if ⎪ 2 6 ⎪ ⎪ ⎪ ⎪ 6 5n ⎩round((n − i) · · 255) if ≤ i ≤ n. n 6 (c) Implement a function that renders a color wheel with an arbitrary number of colors. For example, your function should be able to produce a color wheel with 60 colors like the one below.

5. Weather maps often use color to encode temperatures. Thus, low temperatures are shown using blue tones, medium temperatures using green tones, and high temperatures using red tones. The following definitions implement three color intensity functions with input values between 0◦ C and 100◦ C and output values between 0 and 255: r(T) := 255*(T/100)^1.5 $ b(T) := r(100 - T) $ g(T) := sqrt(255^2 - r(T)^2 - b(T)^2) $ wxdraw2d(xlabel = "temperature", ylabel = "color intensity", color = red, key="hot", explicit(r(T), T, 0, 100), color = green, key="medium", explicit(g(T), T, 0, 100),

EXERCISES

357

color = blue, key="cold", explicit(b(T), T, 0, 100));

These functions can be combined with the pixel() function to create a “swatch” of colors encoding temperatures between 0◦ C and 100◦ C as follows: pixeltemp(x, y, T) := pixel(x, y, round(r(T)), round(g(T)), round(b(T))) $ wxdraw2d(proportional_axes = xy, color = black, xlabel = "", ylabel = "", xtics = none, ytics = none, makelist(pixeltemp(i, 0, 5*i), i, 0, 20));

Modify the finite difference function Fourierplot() described in Section 3.5 so it produces an animation showing a line of colored pixels encoding the progression of temperatures in a metal rod over time. In particular, the pixel at location (i, 0) in the j th frame of the animation should encode the temperature ui,j = u(xi , tj ).

CHAPTER 4

Interpolation and Approximation “I know too much. I know a little about everything and everyone, but I cannot get my pattern. Half of these facts are irrelevant. I want a pattern. A pattern. My kingdom for a pattern!” — Hercule Poirot as quoted in Third Girl by Agatha Christie (1966)

4.1. Lagrange’s Formula In contrast to problems involving sampling, which require us to extract discrete information like sample data points or pixel locations from a continuous function, the problem of interpolation seeks to reconstruct a continuous function from a potentially large collection of discrete data points. In a way, it this type of problem that might induce a certain detective with an egg-shaped head and a splendid waxed mustache to complain about having too much information but no pattern in sight. This pretty well sums up the situation in which Johann Kepler found himself at the start of the fifteenth century. Tycho Brahe (1546 – 1601), his mentor and predecessor at the Uranienborg observatory, had spent the better part of thirty years measuring the position of the various stars and planets, some as accurately as a fraction of a degree. Upon Brahe’s death, it fell on Kepler to take all this data and find a pattern describing their motion. The approach that we shall describe throughout this chapter is reminiscent of a technique commonly used in the ship-building industry known as lofting . In the process of designing a boat’s hull, several key points of the hull would be fixed on the floor of a large design loft using heavy weights known as ducks, since they look a little like a duck’s head, and a thin wooden strip called a spline was passed through them, as shown in Fig. 4-1. The spline would then assume a shape of minimal stress between the ducks, thus providing an interpolation of the key points on the hull.

Fig. 4-1. Lofting ducks holding a wooden spline.

362

4. INTERPOLATION AND APPROXIMATION

In this section, we replicate this idea mathematically by constructing a polynomial with which we might fill in the gaps between a set of data points, which we shall also call ducks. Before addressing the problem in general, let us start off by finding a polynomial spline whose graph goes through a handful of ducks, say (0, 0), (1, 3), and (2, 0). To solve this problem, we will begin with a brute-force approach. We consider a general quadratic polynomial and turn to Maxima’s algebraic solving capabilities to determine what its coefficients ought to be. We proceed as follows: f : a*t^2 + b*t + c; a t2 + b t + c y0 : subst(t = 0, f); c y1 : subst(t = 1, f); c+b+a y2 : subst(t = 2, f); c + 2b + 4a soln : solve([y0 = 0, y1 = 3, y2 = 0]); [[b = 6, a = −3, c = 0]] g : subst(soln, f); 6 t − 3 t2 We can confirm that our endeavors have met with success by plotting our function and noting that it goes through the desired set of ducks: wxdraw2d(grid = true, xlabel = "T", yrange=[-2, 4], color = red, explicit(g, t, -1, 3), color = blue, point_type = circle, points([[0,0], [1,3], [2,0]]));

4.1. LAGRANGE’S FORMULA

363

Notice that factoring the polynomial that we found above suggests a faster and more elegant way to answer the same question: factor(g); −3 (t − 2) t In particular, observe that the factors of t and t − 2 guarantee that g(0) = g(2) = 0, while the factor of −3 ensures that g(1) = 3. This means that we could have instead started with a nonzero function whose graph goes through the two “zero ducks” at t = 0 and t = 2, and then adjusted it so it would also go through the “nonzero duck” at t = 1: h : t * (t-2); (t − 2) t k : subst(t = 1, h); −1 h : 3 * h/k; −3 (t − 2) t Evidently, in just a couple of steps, we arrived at the same solution, but without the hassle of solving any equations. With these ideas in mind, let us consider the slightly more difficult task of finding a polynomial function whose graph goes through more

364

4. INTERPOLATION AND APPROXIMATION

than one nonzero duck, say (0, 1), (1, 0), (2, 3), and (4, 0). As before, we can repeat our brute-force approach, this time starting with a general cubic function and solving an appropriate system of equations to determine the value of its coefficients: f : a*t^3 + b*t^2 + c*t + d; a t3 + b t2 + c t + d y0 : subst(t = 0, f); d y1 : subst(t = 1, f); d+c+b+a y2 : subst(t = 2, f); d + 2c + 4b + 8a y4 : subst(t = 4, f); d + 4 c + 16 b + 64 a soln : solve([y0 = 1, y1 = 0, y2 = 3, y4 = 0]); 37 7 19 , a = − , d = 1]] [[c = − , b = 4 8 8 g : subst(soln, f); 37 t2 19 t 7 t3 + − +1 − 8 8 4 wxdraw2d(grid = true, xlabel = "T", yrange = [-2, 5], color = red, explicit(g, t, -1, 5), color = blue, point_type = circle, points([[0,1], [1,0], [2,3], [4,0]]));

4.1. LAGRANGE’S FORMULA

365

We can also answer the question by a slight modification to our elegant approach from above. For instance, factoring our brute-force solution shows that we can deal with the two zero ducks at t = 1 and t = 4 in the same way as in our earlier example, namely by including factors of t − 1 and t − 4 in the formula for our spline: factor(g); (t − 4) (t − 1) (7 t − 2) − 8 On the other hand, how to deal with the two nonzero ducks at t = 0 and t = 2, and how these account for the extra factor of − 18 (7t − 2), remains for the moment a mystery. The trick, it turns out, is to handle the nonzero ducks one at a time. We first design a function that goes through the first nonzero duck at (0, 1) and is zero when t = 1, t = 2, and t = 4: h1 : (t-1)*(t-2)*(t-4); (t − 1)(t − 2)(t − 4) k : subst(t = 0, h1); −8 h1 : 1 * h1/k; (t − 4) (t − 2) (t − 1) − 8

366

4. INTERPOLATION AND APPROXIMATION

Next, we design another function that goes through the second nonzero duck at (2, 3) and is zero when t = 0, t = 1, and t = 4: h2 : t*(t-1)*(t-4); t(t − 1)(t − 4) k : subst(t=2, h2); −4 h2 : 3 * h2/k; 3 (t − 4) (t − 1) t − 4 Then, the sum of these two functions gives the same polynomial spline found above: h : h1 + h2; 3 (t − 4) (t − 1) t (t − 4) (t − 2) (t − 1) − − 4 8 expand(h); 37 t2 19 t 7 t3 + − +1 − 8 8 4 These computations illustrate a general method, originally discovered by Joseph-Louis Lagrange (1736 – 1813), for interpolating n ducks with a polynomial spline. Given data points at (x1 , y1 ), (x2 , y2 ), . . . , (xn , yn ), the interpolating polynomial is determined by the formula n  ( t − xj yi . f (t) = xi − xj i=1 j=i

Note that this formula consists of a sum of n products, each of which gives a function that goes through one of the ducks in question and is zero for all the other values of xi . A na¨ıve implementation of Lagrange’s formula as two nested loops, one computing each product and the other accumulating the sum of these products, will lead to a quadratic-time function. However, there is an awful lot of repetition involved in this approach. A better implementation starts by computing the single product

4.1. LAGRANGE’S FORMULA

g(t) =

n (

367

(t − xj ) = (t − x1 )(t − x2 )(t − x3 ) · · · (t − xn ),

j=1

which is zero at all values of xi , and then uses this product to construct the desired interpolating function f (t) in linear time as follows: Lagrange(A) := block([t, n, i, f, g, h, k], n : length(A), f : 0, g : 1, /* first loop computes product g(t) = (t - x1)*(t - x2)*...*(t - xn) */ for i : 1 thru n do g : g * (t - A[i][1]), /* second loop computes sum f(t) = h1(t) + h2(t) + ... + hn(t) where hi(t) = ith product in Lagrange’s formula */ for i : 1 thru n do (h : g / (t - A[i][1]), k : subst(t = A[i][1], h), f : f + A[i][2] * h/k), return(expand(f))) $ Observe that our implementation of Lagrange’s formula easily solves our two warm-up problems above: Lagrange([[0, 0], [1, 3], [2, 0]]); 6 t − 3 t2 Lagrange([[0, 1], [1, 0], [2, 3], [4, 0]]); −

7 t3 37 t2 19 t + − +1 8 8 4

368

4. INTERPOLATION AND APPROXIMATION

Table 4-1. Some interesting astronomical data. night

0

1

2

3

4

5

6

7

position

0

3

4

1

0

−3

−1

0

To test the efficacy of Lagrange’s formula, let us imagine that we observe a bright light in the sky over a period of several nights and that, in the spirit of Brahe and Kepler, we carefully record its location in Table 4-1. Before we alert the authorities about our unidentified flying object, let us consider how we might use Lagrange’s formula to construct a continuous function that interpolates this data. In particular, suppose that we compute our interpolating function in several stages, as new data becomes available night after night. In this case, we would see the following sequence of polynomial splines: p3 : Lagrange([[0, 0], [1, 3], [2, 4]]); 4 t − t2 p4 : Lagrange([[0, 0], [1, 3], [2, 4], [3, 1]]); 10 t t3 − 3 3 p5 : Lagrange([[0, 0], [1, 3], [2, 4], [3, 1], [4, 0]]); t4 7 t3 11 t2 4t − + + 3 3 3 3 p6 : Lagrange([[0, 0], [1, 3], [2, 4], [3, 1], [4, 0], [5, -3]]); −

11 t4 91 t3 67 t2 34 t 3 t5 + − + − 20 6 12 6 15

p7 : Lagrange([[0, 0], [1, 3], [2, 4], [3, 1], [4, 0], [5, -3], [6, -1]); 77 t5 103 t4 949 t3 3121 t2 263 t 13 t6 − + − + − 240 80 16 48 120 30

4.1. LAGRANGE’S FORMULA

369

p8 : Lagrange([[0, 0], [1, 3], [2, 4], [3, 1], [4, 0], [5, -3], [6, -1], [7, 0]]); −

97 t6 931 t5 299 t4 3747 t3 6649 t2 623 t t7 + − + − + − 60 240 240 16 80 120 30

Observe that the degree of our interpolations grows with each step. This is not a surprise, of course, since the interpolations must get progressively more complicated as we consider more data. Of particular concern is the relative size of the coefficients involved, some of which become very large while others become quite small: float(p8); −0.01666666666666667 t7 + 0.4041666666666667 t6 −3.879166666666667 t5 + 18.6875 t4 − 46.8375 t3 +55.40833333333333 t2 − 20.76666666666667 t In addition, notice that the overall shape of the interpolating polynomial changes globally when new sample points are added: wxdraw2d(grid = true, xlabel = "T", xrange = [0, 10], yrange = [-5, 5], color = black, point_type = circle, points([[0, 0], [1, 3], [2, 4], [3, 1], [4, 0], [5, -3], [6, -1], [7, 0]]), color = red, key = "3 points", explicit(p3, t, 0, 8), color = orange, key = "4 points", explicit(p4, t, 0, 8), color = yellow, key = "5 points", explicit(p5, t, 0, 8), color = green, key = "6 points", explicit(p6, t, 0, 8), color = blue, key = "7 points", explicit(p7, t, 0, 8), color = violet, key = "8 points", explicit(p8, t, 0, 8));

370

4. INTERPOLATION AND APPROXIMATION

For instance, you will observe that incorporating just a single point can change whether the function eventually heads off to positive or negative infinity. This means that a partial collection of sample points will rarely be sufficient to predict the next location of our unidentified flying object. This behavior is suboptimal at best. To make matters worse, the shape of the left side of the graph changes when new points are added on the right. For example, take particular note of how the local minimum between (0, 0) and (1, 3) and the local maximum between (1, 3) and (2, 4) are amplified as more sample points are added. In contrast, the minimum between (3, 1) and (4, 0) and the maximum between (4, 0) and (5, −3) are lessened. In other words, our understanding of the unidentified flying object’s past trajectory is constantly changing as we acquire new information of its position in the future. Indeed, as we consider more and more points, the interpolating functions only agree on the sample points themselves, and their graphs do not appear to converge at all. This phenomenon seems quite unreasonable. There are several other reasons why interpolating a large set of sample points with a single polynomial spline is simply not desirable. For one thing, Lagrange interpolation is known to be unstable, meaning that a small change in the data can result in a major change in the interpolating function. This is illustrated in Fig. 4-2, which shows the result of interpolating a set in which the sample points have been deviated by a random error of −0.1 ≤ ≤ 0.1 from the x-axis; you will observe the wild oscillations that emerge, particularly near the ends of the domain.

4.1. LAGRANGE’S FORMULA

371

Fig. 4-2. Lagrange interpolation is unstable. In addition, when relatively stable data makes a sudden jump, as in Fig. 4-3, the resulting interpolation will exhibit a behavior known as ringing , in which the function overshoots the otherwise stable data with waves of increasing amplitude. For all of these reasons, we will need to consider alternative solutions to the interpolation problem. In the next sections, we will show how “local” solutions can be pieced together to form a global solution and avoid many of the pitfalls described above.

Fig. 4-3. Lagrange interpolation can suffer from ringing.

Exercises for Section 4.1 1. Write a function that implements Lagrange’s interpolation formula as two nested loops, an inner one for computing each product and an outer one for computing the sum of the products. Explain why your function has a quadratic running time. 2. Let qi (t) denote the four Lagrange cubic splines determined by the following table: spline

value at t=0

value at t=1

value at t=2

value at t=3

q0 (t)

1

0

0

0

q1 (t)

0

1

0

0

q2 (t)

0

0

1

0

q3 (t)

0

0

0

1

(a) Find the formulas for q0 (t), q1 (t), q2 (t), and q3 (t). (b) Compute the value of the sum f (t) = q0 (t) + q1 (t) + q2 (t) + q3 (t). (c) Explain why your answer to part (b) does not appear to depend on t, even though each polynomial qi (t) does. In other words, why do the t’s all cancel out in this sum? Hint: It might help to think of f (t) as a Lagrange polynomial spline on its own right. What collection of ducks would f (t) be interpolating? 3. As the German mathematician Carl Runge discovered, some functions are averse to interpolation by polynomials, even when the number of samples used is very large. This behavior, which is commonly known as Runge’s phenomenon, is most evident in functions like f (t) =

1 . 1 + 25t2

For such functions, increasing the degree of the interpolation does not always improve the accuracy of the interpolation. (a) Define a function Runge() that takes as input a positive integer n and returns a list of ducks of the form         t0 , f (t0 ) , t1 , f (t1 ) , t2 , f (t2 ) , . . . , tn , f (tn ) ,

EXERCISES

373

where ti = −1 + 2i/n. (b) Plot the Lagrange interpolation of the ducks produced by your function from part (a), alongside the graph of f (x) for a variety of values of n. What do you observe? Does the interpolation get closer or farther apart when the value of n is increased? 4. Write a function that takes as input an integer k and produces the graph of the Lagrange polynomial spline interpolating the following collection of ducks:             0, 0 , 1, 0 , 2, 0 , . . . , k − 1, 0 , k, 1 , . . . , 2k − 1, 1 . The spline graphed by your function should approximate the “ramp-up” function ⎧ ⎪ if t < k − 1, ⎨0 rk (t) = t − k + 1 if k − 1 ≤ t < k, ⎪ ⎩ 1 if t ≥ k. How close is the approximation? Does the approximation get better or worse when the value of k is increased? 5. Design a function that takes as input an integer n and produces the graph of the Lagrange polynomial spline interpolating n + 1 “noisy” ducks of the form         0, 0 , 1, 1 , 2, 2 , . . . , n, n , where each k is a random number between −0.1 and 0.1. Run the function several times. Does the resulting curve always stay relatively close to the x-axis? In particular, does it stay within 0.1 of the axis, even between ducks? Explain. Hint: Recall that if x is a positive floating-point number, then random(x) returns a floating-point number between zero and x.

4.2. Piecewise Linear Interpolation Since interpolation by a single polynomial has proven impractical for a variety of reasons, we shall now consider a piecewise linear interpolation. Graphically, all this requires is connecting the various data points with straight line segments. For instance, we can produce a piecewise linear interpolation of our astronomical data from Table 41 (see page 368) by calling on the points() graphic constructor with the points_joined option set to true as follows: UFOdata : [[0, 0], [1, 3], [2, 4], [3, 1], [4, 0], [5, -3], [6, -1], [7, 0]] $ wxdraw2d(grid = true, xlabel = "T", points_joined = true, point_type = dot, points(UFOdata));

To find a formula that describes this interpolation, we shall use a built-in version of Lagrange’s formula supplied by Maxima’s interpol package. load(interpol) $ With this function we can obtain a global polynomial interpolator for our data set:

376

4. INTERPOLATION AND APPROXIMATION

lagrange(UFOdata, varname = t); (t − 7) (t − 5) (t − 4) (t − 3) (t − 2) (t − 1) t 720 (t − 7) (t − 6) (t − 4) (t − 3) (t − 2) (t − 1) t − 80 (t − 7) (t − 6) (t − 5) (t − 4) (t − 2) (t − 1) t + 144 (t − 7) (t − 6) (t − 5) (t − 4) (t − 3) (t − 1) t − 60 (t − 7) (t − 6) (t − 5) (t − 4) (t − 3) (t − 2) t + 240 Alternatively, we can apply Lagrange’s interpolation formula to each pair of consecutive points and then collect the results in a single piecewise linear function: pieces : makelist(expand(lagrange([UFOdata[i], UFOdata[i+1]], varname = t)), i, 1, 7); [3t, t + 2, 10 − 3t, 4 − t, 12 − 3t, 2t − 13, t − 7] Evidently, the formula for our piecewise linear interpolating function is ⎧ ⎪ if 0 ≤ t < 1, ⎪3t ⎪ ⎪ ⎪ ⎪ ⎪ t+2 if 1 ≤ t < 2, ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ 10 − 3t if 2 ≤ t < 3, ⎪ ⎪ ⎨ if 3 ≤ t < 4, f (t) = 4 − t ⎪ ⎪ ⎪ ⎪ 12 − 3t if 4 ≤ t < 5, ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ 2t − 13 if 5 ≤ t < 6, ⎪ ⎪ ⎪ ⎪ ⎪ ⎩t − 7 if 6 ≤ t ≤ 7. One approach for implementing this piecewise linear function in Maxima is to use the interpol function charfun2(). This function takes as input a variable name and two numeric values and produces a characteristic function that returns an output value of 1 when the variable is between the input values and of 0 otherwise. For instance, the expression charfun2(t, 0, 1)

4.2. PIECEWISE LINEAR INTERPOLATION

377

corresponds to the characteristic function ⎧ ⎨1 if 0 ≤ t < 1, χ(t) = ⎩0 otherwise. wxdraw2d(grid = true, xlabel = "T", yrange = [-0.5, 1.5], explicit(charfun2(t, 0, 1), t, -1, 2));

In particular, our piecewise linear interpolating function above can be constructed as a linear combination of characteristic functions, using as coefficients the formulas that we stored in pieces: f : sum(pieces[i] * charfun2(t, i-1, i), i, 1, 7) $ wxdraw2d(grid = true, xlabel = "T", color = red, explicit(f, t, 0, 7), color = blue, point_type = circle, points(UFOdata));

378

4. INTERPOLATION AND APPROXIMATION

In fact, the linearinterpol() function, which is also part of the interpol package, constructs a piecewise linear interpolation with precisely this form: f : linearinterpol(UFOdata, varname = t); 3t charfun2 (t, −∞, 1) + (t − 7) charfun2 (t, 6, ∞) + (2t − 13) charfun2 (t, 5, 6) + (12 − 3t) charfun2 (t, 4, 5) + (4 − t) charfun2 (t, 3, 4) + (10 − 3t) charfun2 (t, 2, 3) + (t + 2) charfun2 (t, 1, 2) Another way to implement the piecewise linear interpolating function f (t) is to examine the linear pieces produced by Lagrange’s formula in their original form: ⎧ ⎪ 0 · (1 − t) + 3 · t if 0 ≤ t < 1, ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ 3 · (2 − t) + 4 · (t − 1) if 1 ≤ t < 2, ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ 4 · (3 − t) + 1 · (t − 2) if 2 ≤ t < 3, ⎪ ⎪ ⎨ if 3 ≤ t < 4, f (t) = 1 · (4 − t) + 0 · (t − 3) ⎪ ⎪ ⎪ ⎪ 0 · (5 − t) − 3 · (t − 4) if 4 ≤ t < 5, ⎪ ⎪ ⎪ ⎪ ⎪ ⎪ ⎪−3 · (6 − t) − 1 · (t − 5) if 5 ≤ t < 6, ⎪ ⎪ ⎪ ⎪ ⎩−1 · (7 − t) + 0 · (t − 6) if 6 ≤ t ≤ 7,

4.2. PIECEWISE LINEAR INTERPOLATION

379

Note that each output value that we wish to interpolate appears twice in this formula: once on the linear piece preceding the corresponding input value and once on the linear piece following it. If we isolate each of these terms, we obtain a collection of six spike functions, each of which is zero at all but one integer input value. For instance, the spike function at t = k is given by the formula ⎧ ⎪ 0 if t < k − 1, ⎪ ⎪ ⎪ ⎪ ⎪ ⎨t − k + 1 if k − 1 ≤ t < k, sk (t) = ⎪ 1 + k − t if k ≤ t < k + 1, ⎪ ⎪ ⎪ ⎪ ⎪ ⎩0 if t ≥ k + 1. We can implement each one of these spike functions as the sum of two characteristic functions as follows: linearinterpol([[k-2, 0], [k-1, 0], [k, 1], [k+1, 0], [k+2, 0]], varname = t); (−t + k + 1) charfun2 (t, k, k + 1) + (t − k + 1) charfun2 (t, k − 1, k) s(k, t) := ’’% $ We can plot the set of spikes {s1 (t), s2 (t), s3 (t), s4 (t), s5 (t), s6 (t)} by entering: wxdraw2d(grid = true, xlabel = "T", yrange = [-0.1, 1.1], color=red, key="s1(t)", explicit(s(1, t), t, 0, 9), color=orange, key="s2(t)", explicit(s(2, t), t, 0, 9), color=yellow, key="s3(t)", explicit(s(3, t), t, 0, 9), color=green, key="s4(t)", explicit(s(4, t), t, 0, 9), color=blue, key="s5(t)", explicit(s(5, t), t, 0, 9), color=violet, key="s6(t)", explicit(s(6, t), t, 0, 9));

380

4. INTERPOLATION AND APPROXIMATION

Our interpolation function f (t) can be rewritten as a linear combination of these spike functions, where the coefficients are precisely the output values that we wish to interpolate: f : 3 * s(1, t) + 4 * s(2, t) + 1 * s(3, t) + 0 * s(4, t) + (-3) * s(5, t) + (-1) * s(6, t) $ wxdraw2d(grid = true, xlabel = "T", color = red, explicit(f, t, 0, 7), color = blue, point_type=circle, points(UFOdata));

4.2. PIECEWISE LINEAR INTERPOLATION

381

In fact, it turns out that the six spike functions above can be combined to construct the piecewise linear interpolation of any set of data with input values at t = 1, t = 2, t = 3, t = 4, t = 5, and t = 6. We will denote the space of all such interpolation functions as S6 . Perhaps you will wonder about the unfamiliar way in which we are using the word “space” here. Mathematicians typically use the word “space” to refer to a set with some sort of algebraic or geometric structure. In the case of the function space S6 , there is both an algebraic structure, in the sense that functions in this space can be multiplied by scalars and added together, and a geometric structure, in the sense that we can talk about functions in this space being close together or far away from one another. The collection of spike functions {s1 , s2 , s3 , s4 , s5 , s6 } plays the same role in S6 as the collection of standard unit vectors {i, j, k} does in three-dimensional euclidean space R3 . In particular, every interpolation function in S6 can be constructed as a unique linear combination of these spike functions. For this reason, we will say that the six spike functions form a basis for S6 . We can encode the coefficients in such a linear combination by a row vector as follows: V : matrix([3, 4, 1, 0, -3, -1]); $ % 3 4 1 0 −3 −1 The required linear combination can then be reconstructed by multiplying this row vector times a column vector of spike functions. For example, consider the following line of symbolic computation: V.matrix([s1], [s2], [s3], [s4], [s5], [s6]); −s6 − 3 s5 + s3 + 4 s2 + 3 s1 Repeating the multiplication above using a column vector of veritable spike functions, we obtain our piecewise linear interpolation in its most succinct form of V.S: S : matrix([s(1, t)], [s(2, t)], [s(3, t)], [s(4, t)], [s(5, t)], [s(6, t)]) $ wxdraw2d(grid = true, xlabel = "T", color = red, explicit(V.S, t, 0, 7),

382

4. INTERPOLATION AND APPROXIMATION

color = blue, point_type=circle, points(UFOdata));

Before moving on, we should note that piecewise linear interpolation resolves many of the objections raised during our exploration of Lagrange polynomials. For instance, piecewise linear interpolation is stable and does not suffer the effects of ringing. In addition, earlier pieces of the interpolation remain unchanged when new sample points are considered. Unfortunately, the graphs produced by this method of interpolation still seem inadequate models for any realistic trajectory. Everybody knows that unidentified flying objects simply do not travel in straight lines and then turn sharply at odd angles, except perhaps in antique video games or low-budget science fiction films. What is required is an approach that combines the aesthetic curvature found in polynomial interpolation with the stability of piecewise linear interpolation. This will be our goal in the next few sections.

Exercises for Section 4.2 1. Write a Maxima function that takes as input a list of values [y0 , y1 , y2 , . . . , yn ] and computes the piecewise linear interpolation of the following collection of ducks: 

       0, y0 , 1, y1 , 2, y2 , . . . , n, yn .

Your function should return a list containing the formulas for each of the n linear pieces of the interpolation. 2. Consider the collection of ramp-up functions given by the formula ⎧ ⎪ if t < k − 1, ⎨0 rk (t) = t − k + 1 if k − 1 ≤ t < k, ⎪ ⎩ 1 if t ≥ k. (a) Implement each of the functions {r1 , r2 , r3 , r4 , r5 , r6 , r7 } using the charfun() command. (b) Express the interpolation linear function f (t) described in this section as a linear combination of finitely many rampup functions rk (t). What is the geometric significance of the coefficients used in such a linear combination? (c) What is the relationship between the coefficients used to express f (t) as a linear combination of spike functions and those used to express it as a linear combination of ramp-up functions? 3. Consider the spike functions sk (t) defined in this section and the ramp-up functions rk (t) defined in Exercise 2. (a) Show that every spike function sk (t) can be expressed as a linear combination of finitely many ramp-up functions rk (t). Express this linear combination as the product of a row vector of coefficients and a column vector of ramp-up functions. (b) Explain why the ramp-up function rk (t) cannot be expressed as a combination of finitely many spike functions sk (t). 4. Consider the space R7 of piecewise functions constructed as linear combinations of the ramp-up functions {r1 , r2 , r3 , r4 , r5 , r6 , r7 } defined in Exercise 2. (a) Show that every function in the space S6 is contained in the space R7 . (b) Show that the space R7 is larger than the space S6 . In other words, show that there are functions in R7 which are not

384

4. INTERPOLATION AND APPROXIMATION

contained in S6 . How would you characterize these functions? 5. Design a function that takes as input an integer n and produces the graph of the piecewise linear interpolation of n + 1 “noisy” ducks of the form         0, 0 , 1, 1 , 2, 2 , . . . , n, n , where each k is a random number between −0.1 and 0.1. Run the function several times. Does the resulting curve always stay relatively close to the x-axis? In particular, does it stay within 0.1 of the axis, even between ducks? Explain.

4.3. Cubic Splines Recall that we started this chapter by investigating “global” polynomial functions that interpolate a large set of sample points, and then we considered “local” piecewise linear interpolators that connect only a pair of points at a time. The former were smooth, but suffered from a variety of instabilities. In contrast, the latter resolved many of the instability issues, but had sharp angles at each one of the interpolated points. We shall now try to combine different aspects of these two approaches to the question of interpolation. In an effort to find a solution to the interpolation problem that is both smooth and stable, we once again turn to our Lagrange interpolation formula, but this time do so with an eye toward local interpolation. In particular, we will construct four Lagrange cubic splines, each interpolating a quadruple of ducks as follows: q0 (t)

interpolates

(0, 1), (1, 0), (2, 0), (3, 0),

q1 (t)

interpolates

(0, 0), (1, 1), (2, 0), (3, 0),

q2 (t)

interpolates

(0, 0), (1, 0), (2, 1), (3, 0),

q3 (t)

interpolates

(0, 0), (1, 0), (2, 0), (3, 1).

To construct these polynomials we turn to the implementation of Lagrange’s formula provided in Maxima’s interpol package: load(interpol) $ q0 : expand(lagrange([[0, 1], [1, 0], [2, 0], [3, 0]], varname = t)); −

t3 11 t + t2 − +1 6 6

q1 : expand(lagrange([[0, 0], [1, 1], [2, 0], [3, 0]], varname = t)); t3 5 t2 − + 3t 2 2

386

4. INTERPOLATION AND APPROXIMATION

q2 : expand(lagrange([[0, 0], [1, 0], [2, 1], [3, 0]], varname = t)); 3t t3 − + 2 t2 − 2 2 q3 : expand(lagrange([[0, 0], [1, 0], [2, 0], [3, 1]], varname = t)); t3 t2 t − + 6 2 3 wxdraw2d(grid = true, xlabel = "T", yrange = [-0.4, 1.6], color = red, key = "q0(t)", explicit(q0, t, 0, 3), color = green, key = "q1(t)", explicit(q1, t, 0, 3), color = blue, key = "q2(t)", explicit(q2, t, 0, 3), color = violet, key = "q3(t)", explicit(q3, t, 0, 3));

The Lagrange cubic splines form a basis for the space P3 of cubic polynomials,1 much like the spike functions sk (t) did for the space S6 1 In the discussion that follows, we shall refer to any polynomial of degree less than or equal to three as a cubic polynomial, even though it might be a quadratic, linear, or even constant function.

4.3. CUBIC SPLINES

387

of piecewise linear functions in Section 4.2. This new basis allows us to interpolate any quadruple of ducks placed at t = 0, t = 1, t = 2, and t = 3. For example, suppose that we place four ducks at (0, 0), (1, 3), (2, 4), and (3, 1). Then we can find an interpolation for these ducks by taking the linear combination of the cubic splines determined by the duck’s output values, as follows: f0 : 0*q0 + 3*q1 + 4*q2 + 1*q3;  3   3  t t3 5 t2 t t 3t t2 2 +3 − + 3t + 4 − + 2t − − + 6 2 2 2 2 2 3 expand(f0); 10 t t3 − 3 3 wxdraw2d(grid = true, xlabel = "T", color = red, explicit(f0, t, 0, 3), color = blue, point_type = circle, points([[0, 0], [1, 3], [2, 4], [3, 1]]));

Furthermore, we can implement this linear combination as the product of a row vector containing data values and a column vector containing

388

4. INTERPOLATION AND APPROXIMATION

interpolating splines. Thus, in the case of the data points above, we will have: V : matrix([0, 3, 4, 1]); $ % 0 3 4 1 Q : matrix([q0], [q1], [q2], [q3]); ⎤ ⎡ t3 11 t 2 +1 ⎥ ⎢ − +t − 6 6 ⎥ ⎢ ⎥ ⎢ 3 2 ⎥ ⎢ t 5 t ⎥ ⎢ − + 3t ⎥ ⎢ 2 2 ⎥ ⎢ ⎥ ⎢ 3 t 3 t ⎥ ⎢ ⎥ ⎢ − + 2 t2 − ⎢ 2 2 ⎥ ⎥ ⎢ ⎦ ⎣ t2 t t3 − + 6 2 3 expand(V.Q); 10 t t3 − 3 3 The basis of Lagrange cubic splines can be further encoded by arranging their coefficients in a 4 × 4 matrix, with q0 (t) along the top row, q1 (t) along the second row, q2 (t) along the third row, and q3 (t) along the bottom row: L : matrix([-1/6, 1, -11/6, 1], [1/2, -5/2, 3, 0], [-1/2, 2, -3/2, 0], [1/6, -1/2, 1/3, 0]); ⎤ ⎡ 1 11 1 − 1⎥ ⎢ − ⎥ ⎢ 6 6 ⎥ ⎢ ⎥ ⎢ 1 5 ⎥ ⎢ − 3 0 ⎥ ⎢ 2 2 ⎥ ⎢ ⎥ ⎢ 3 1 ⎥ ⎢ 2 − 0⎥ ⎢ − ⎥ ⎢ 2 2 ⎥ ⎢ ⎦ ⎣ 1 1 1 − 0 6 2 3 Note that the first column in this matrix corresponds to the coefficients of t3 in each spline. Similarly, the second column corresponds

4.3. CUBIC SPLINES

389

to the coefficients of t2 , the third column to the coefficients of t, and the fourth column to the constant coefficients. Therefore, when this matrix is multiplied on the right by a column vector consisting of powers of t, we will recover our original vector of cubic splines: T : matrix([t^3], [t^2], [t], [1]); ⎡ ⎤ t3 ⎢ ⎥ ⎢ 2⎥ ⎢t ⎥ ⎢ ⎥ ⎢ ⎥ ⎢ t ⎥ ⎣ ⎦ 1 L.T; ⎡

t3 11 t +1 ⎢ − + t2 − ⎢ 6 6 ⎢ ⎢ t3 5 t2 ⎢ − + 3t ⎢ ⎢ 2 2 ⎢ ⎢ t3 3t ⎢ ⎢ − + 2 t2 − ⎢ 2 2 ⎢ ⎢ 3 2 t t t ⎣ − + 6 2 3

⎤ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎦

In contrast, multiplying on the left times our data vector results in a row vector that contains the coefficients of the interpolating polynomial we found above: V.L; ) 1 − 0 3

10 3

* 0

In either case, one more multiplication gives us our interpolating function: expand(V.L.T); 10 t t3 − 3 3

390

4. INTERPOLATION AND APPROXIMATION

The computation above is known as a change of basis. We normally prefer to describe the space P3 in terms of the standard basis {t3 , t2 , t, 1}. However, when interpolating data, the basis of Lagrange cubic splines is much more convenient. Therefore, when we multiply V.L.T, we are employing the change-of-basis matrix L to translate from our standard basis to our basis of splines, which are then combined into the desired interpolation function: −→

T standard basis

L.T

−→

V.L.T

basis of splines

interpolation

Alternatively, we can think of the matrix L as converting the coefficients in a linear combination of cubic splines (i.e., the output values used for the interpolation) into the coefficients in the linear combination of powers of t constituting the interpolating function: V

−→

spline coefficients

V.L

−→

standard coefficients

V.L.T interpolation

Regardless of how we think about it, the product of the three matrices produces the interpolating function almost as if by magic. Let us explore our approach further by considering another round of interpolation, say for a set of ducks at (1, 3), (2, 4), (3, 1), and (4, 0). Proceeding as above, we enter: V1 : matrix([3, 4, 1, 0]); $ % 3 4 1 0 f1 : expand(V1.L.T); t 3 − 5 t2 + 5 t + 3 wxdraw2d(grid = true, xlabel = "T", color = red, explicit(f1, t, 0, 4), color = blue, point_type = circle, points([[1, 3], [2, 4], [3, 1], [4, 0]]));

4.3. CUBIC SPLINES

391

Observe that the resulting interpolation misses its target, always passing one unit to the left of where it ought to! The reason, of course, is that our ducks are not where the splines were expecting them to be. Our solution will be to employ another change-of-basis matrix, this time switching from the standard basis of powers of t to a basis of powers of t − 1. Just like a linear transformation taking a function f (t) → f (t − 1), this change of basis will shift the graph of our interpolation one unit to the right. We begin by expanding the polynomials in our new basis and encoding the results in a 4 × 4 matrix of coefficients as follows: makelist(expand((t-1)^(3-k)), k, 0, 3); [t3 − 3t2 + 3t − 1, t2 − 2t + 1, t − 1, 1] S : matrix([1, -3, 3, -1], [0, 1, -2, 1], [0, 0, 1, -1], [0, 0, 0, 1]); ⎤ ⎡ 1 −3 3 −1 ⎥ ⎢ ⎥ ⎢ ⎢ 0 1 −2 1 ⎥ ⎥ ⎢ ⎥ ⎢ 1 −1 ⎥ ⎢0 0 ⎦ ⎣ 0 0 0 1 As before, multiplying on the right times T reveals the polynomials

392

4. INTERPOLATION AND APPROXIMATION

encoded by this matrix. Furthermore, multiplying on the left times L reveals a new collection of cubic splines. As we will see, these new splines constitute a basis suitable for interpolating a set of ducks placed at t = 1, t = 2, t = 3, and t = 4. All that is required is an additional matrix multiplication: expand(L.S.T); ⎡ t3 3 t2 13 t ⎢ − 6 + 2 − 3 +4 ⎢ ⎢ t3 19 t 2 ⎢ ⎢ 2 − 4t + 2 − 6 ⎢ 3 2 ⎢ ⎢ − t + 7t − 7t + 4 ⎢ 2 2 ⎢ ⎣ t3 11 t − t2 + −1 6 6

⎤ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎦

f1 : expand(V1.L.S.T); t3 − 8 t2 + 18 t − 8 wxdraw2d(grid = true, xlabel = "T", color = red, explicit(f1, t, 1, 4), color = blue, point_type = circle, points([[1, 3], [2, 4], [3, 1], [4, 0]]));

4.3. CUBIC SPLINES

393

We can continue interpolating the rest of the data from Table 4-1 (page 368) in the same fashion, taking four points at a time and performing an appropriate number of shifts: V2 : matrix([4, 1, 0, -3]); $ % 4 1 0 −3 f2 : expand(V2.L.S.S.T); 76 t 2 t3 + 7 t2 − + 32 − 3 3 V3 : matrix([1, 0, -3, -1]); $ % 1 0 −3 −1 f3 : expand(V3.L.S.S.S.T); 7 t3 365 t − 15 t2 + − 78 6 6 wxdraw2d(grid = true, xlabel = "T", color = red, explicit(f0, t, 0, 3), color = orange, explicit(f1, t, 1, 4), color = yellow, explicit(f2, t, 2, 5), color = green, explicit(f3, t, 3, 6), color = blue, point_type = circle, points([[0, 0], [1, 3], [2, 4], [3, 1], [4, 0], [5, -3], [6, -1]]));

394

4. INTERPOLATION AND APPROXIMATION

Note that, by itself, each piece of the interpolation provides a pleasing, smooth curve. Unfortunately, the various pieces do not agree with one another where they overlap. For instance, we find that we have three different choices for how to connect the middle duck at (3, 1) with each of its neighbors. It would certainly be difficult to decide which of these is the path actually taken by our unidentified flying object. All of this work suggests a better approach: to interpolate quadruples having only one duck in common, and then to stitch the resulting curves together. For example, using the data from our example above, we would only consider the first and last polynomials (f0 and f3), which we combine into a piecewise cubic interpolation as follows: wxdraw2d(grid = true, xlabel = "T", color = red, explicit(f0, t, 0, 3), explicit(f3, t, 3, 6), color = blue, point_type = circle, points([[0, 0], [1, 3], [2, 4], [3, 1], [4, 0], [5, -3], [6, -1]]));

4.3. CUBIC SPLINES

395

We can automate this strategy with the following function, which takes a list of output values and produces the corresponding piecewise cubic spline interpolation: cubicspline(A) := block([n, M, plots, i, f, t], n : length(A), M : L, plots : [], /* loop interpolates four consecutive ducks at a time */ for i : 1 thru n-3 step 3 do (f : matrix([A[i], A[i+1], A[i+2], A[i+3]]).M.T, plots : cons(explicit(f, t, i-1, i+2), plots), print("If ", i-1, "