Cambridge PythonForScientists
Cambridge PythonForScientists
Second Edition
Scientific Python is a significant public domain alternative to expensive proprietary soft-
ware packages. This book teaches from scratch everything the working scientist needs
to know using copious, downloadable, useful and adaptable code snippets. Readers will
discover how easy it is to implement and test non-trivial mathematical algorithms and
will be guided through the many freely available add-on modules. A range of exam-
ples, relevant to many different fields, illustrate the language’s capabilities. The author
also shows how to use pre-existing legacy code (usually in Fortran77) within the Python
environment, thus avoiding the need to master the original code.
In this new edition, several chapters have been rewritten to reflect the IPython note-
book style. With an extended index, an entirely new chapter discussing SymPy and a
substantial increase in the number of code snippets, researchers and research students
will be able to quickly acquire all the skills needed for using Python effectively.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241
Python for Scientists
Second Edition
J O H N M . S T E WA RT
Department of Applied Mathematics & Theoretical Physics
University of Cambridge
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241
University Printing House, Cambridge CB2 8BS, United Kingdom
One Liberty Plaza, 20th Floor, New York, NY 10006, USA
477 Williamstown Road, Port Melbourne, VIC 3207, Australia
4843/24, 2nd Floor, Ansari Road, Daryaganj, Delhi – 110002, India
79 Anson Road, #06–04/06, Singapore 079906
www.cambridge.org
Information on this title: www.cambridge.org/9781316641231
DOI: 10.1017/9781108120241
c John M. Stewart 2014, 2017
This publication is in copyright. Subject to statutory exception
and to the provisions of relevant collective licensing agreements,
no reproduction of any part may take place without the written
permission of Cambridge University Press.
First published 2014
Second edition 2017
Printed in the United Kingdom by TJ International Ltd. Padstow Cornwall
A catalogue record for this publication is available from the British Library.
Library of Congress Cataloging-in-Publication Data
Names: Stewart, John, 1943 July 1–
Title: Python for scientists / John M. Stewart, Department of Applied, Mathematics & Theoretical
Physics, University of Cambridge.
Description: Second edition. | Cambridge, United Kingdom ; New York, NY, USA : Cambridge
University Press, [2017] | Includes bibliographical references and index.
Identifiers: LCCN 2016049298 | ISBN 9781316641231 (paperback)
Subjects: LCSH: Science–Data processing. | Python (Computer program language)
Classification: LCC Q183.9 .S865 2017 | DDC 005.13/3–dc23
LC record available at https://lccn.loc.gov/2016049298
ISBN 978-1-316-64123-1 Paperback
Additional resources for this publication at www.cambridge.org/9781316641231
Cambridge University Press has no responsibility for the persistence or accuracy
of URLs for external or third-party Internet Web sites referred to in this publication
and does not guarantee that any content on such Web sites is, or will remain,
accurate or appropriate.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241
Contents
1 Introduction 1
1.1 Scientific Software 1
1.2 The Plan of This Book 4
1.3 Can Python Compete with Compiled Languages? 8
1.4 Limitations of This Book 9
1.5 Installing Python and Add-ons 9
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241
vi Contents
3.5.6 Strings 33
3.5.7 Dictionaries 33
3.6 Python if Statements 34
3.7 Loop Constructs 35
3.7.1 The Python for loop 35
3.7.2 The Python continue statement 37
3.7.3 The Python break statement 37
3.7.4 List comprehensions 38
3.7.5 Python while loops 39
3.8 Functions 39
3.8.1 Syntax and scope 40
3.8.2 Positional arguments 43
3.8.3 Keyword arguments 43
3.8.4 Variable number of positional arguments 43
3.8.5 Variable number of keyword arguments 44
3.8.6 Python input/output functions 44
3.8.7 The Python print function 45
3.8.8 Anonymous functions 47
3.9 Introduction to Python Classes 47
3.10 The Structure of Python 50
3.11 Prime Numbers: A Worked Example 51
4 NumPy 55
4.1 One-Dimensional Arrays 57
4.1.1 Ab initio constructors 57
4.1.2 Look-alike constructors 58
4.1.3 Arithmetical operations on vectors 59
4.1.4 Ufuncs 60
4.1.5 Logical operations on vectors 62
4.2 Two-Dimensional Arrays 65
4.2.1 Broadcasting 65
4.2.2 Ab initio constructors 66
4.2.3 Look-alike constructors 68
4.2.4 Operations on arrays and ufuncs 69
4.3 Higher-Dimensional Arrays 69
4.4 Domestic Input and Output 69
4.4.1 Discursive output and input 70
4.4.2 NumPy text output and input 71
4.4.3 NumPy binary output and input 72
4.5 Foreign Input and Output 73
4.5.1 Small amounts of data 73
4.5.2 Large amounts of data 73
4.6 Miscellaneous Ufuncs 74
4.6.1 Maxima and minima 74
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241
Contents vii
5 Two-Dimensional Graphics 82
5.1 Introduction 82
5.2 Getting Started: Simple Figures 83
5.2.1 Front-ends 83
5.2.2 Back-ends 83
5.2.3 A simple figure 84
5.2.4 Interactive controls 86
5.3 Object-Oriented Matplotlib 87
5.4 Cartesian Plots 88
5.4.1 The Matplotlib plot function 88
5.4.2 Curve styles 89
5.4.3 Marker styles 90
5.4.4 Axes, grid, labels and title 90
5.4.5 A not-so-simple example: partial sums of Fourier series 91
5.5 Polar Plots 93
5.6 Error Bars 94
5.7 Text and Annotations 95
5.8 Displaying Mathematical Formulae 96
5.8.1 Non-LATEX users 96
5.8.2 LATEX users 97
5.8.3 Alternatives for LATEX users 98
5.9 Contour Plots 98
5.10 Compound Figures 101
5.10.1 Multiple figures 101
5.10.2 Multiple plots 102
5.11 Mandelbrot Sets: A Worked Example 104
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241
viii Contents
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241
Contents ix
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241
x Contents
References 250
Index 253
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241
Preface to the Second Edition
The motivation for writing this book, and the acknowledgements of the many who have
assisted in its production, are included in the topics of the Preface to the first edition,
which is reprinted after this one. Here I also need to adjoin thanks to the many readers
who provided constructive criticisms, most of which have been incorporated in this
revision. The purpose here is to explain why a second edition is needed. Superficially it
might appear that very little has changed, apart from a new Chapter 7 which discusses
SymPy, Python’s own computer algebra system.
There is, however, a fundamental change, which permeates most of the latest version
of this book. When the first edition was prepared, the reliable way to use the enhanced
interpreter IPython was via the traditional “terminal mode”. Preparations were under
way for an enhanced “notebook mode”, which looked then rather like the Mathemat-
ica notebook concept, except that it appeared within one’s default web browser.1 That
project has now morphed into the Jupyter notebook. The notebook allows one to con-
struct and distribute documents containing computer code (over forty languages are
supported), equations, explanatory text, figures and visualizations. Since this is also
perhaps the easiest software application for a beginner to develop Python experience,
much of the book has been rewritten for the notebook user. In particular there is now
a lightning course on how to use the notebook in Appendix A, and Chapter 2 has been
extensively rewritten to demonstrate its properties. All of the material in the book now
reflects, where appropriate, its use. For example, it allows SymPy to produce algebraic
expressions whose format is unsurpassed by other computer algebra systems.
This change also affects the areas of interactive graphics and visual animations. Their
demands are such that the standard Python two-dimensional graphics package Mat-
plotlib is having difficulty in producing platform-independent results. Indeed, because
of “improved” software upgrades, the code suggested for immediate on-screen anima-
tions in the first edition no longer works. However, the notebook concept has a subtle
solution to resolve this impasse. Recall that the notebook window is your browser win-
dow, which uses modern HTML graphics. The consequent benefits are introduced in
Chapter 6.
As a final enhancement, all but the most trivial code snippets listed in this book are
now available in electronic form, as a notebook of course, but the website includes
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.001
xii Preface to the Second Edition
HTML and PDF versions, see Section 1.2. The explanatory text surrounding the text is
not included. For that you have to read the book, in hard copy or ebook format!
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.001
Preface to the First Edition
I have used computers as an aid to scientific research for over 40 years. During that
time, hardware has become cheap, fast and powerful. However, software relevant to the
working scientist has become progressively more complicated. My favourite textbooks
on Fortran90 and C++ run to 1200 and 1600 pages respectively. And then we need doc-
umentation on mathematics libraries and graphics packages. A newcomer going down
this route is going to have to invest significant amounts of time and energy in order to
write useful programmes. This has led to the emergence of “scientific packages” such
as Matlab® or Mathematica® which avoid the complications of compiled languages,
separate mathematics libraries and graphics packages. I have used them and found them
very convenient for executing the tasks envisaged by their developers. However, I also
found them very difficult to extend beyond these boundaries, and so I looked for alter-
native approaches.
Some years ago, a computer science colleague suggested that I should take a look at
Python. At that time, it was clear that Python had great potential but a very flaky imple-
mentation. It was, however, free and open-source, and was attracting what has turned
out to be a very effective army of developers. More recently, their efforts have coordi-
nated to produce a formidable package consisting of a small core language surrounded
by a wealth of add-on libraries or modules. A select group of these can and do replicate
the facilities of the conventional scientific packages. More importantly an informed, in-
telligent user of Python and its modules can carry out major projects usually entrusted
to dedicated programmers using Fortran, C etc. There is a marginal loss of execution
speed, but this is more than compensated for by the vastly telescoped development time.
The purpose of this book is to explain to working scientists the utility of this relatively
unknown resource.
Most scientists will have some computer familiarity and programming awareness,
although not necessarily with Python, and I shall take advantage of this. Therefore,
unlike many books which set out to “teach” a language, this one is not just a brisk trot
through the reference manuals. Python has many powerful but unfamiliar facets, and
these need more explanation than the familiar ones. In particular, if you encounter in
this text a reference to the “beginner” or the “unwary”, it signifies a point which is not
made clear in the documentation, and has caught out this author at least once.
The first seven chapters, plus Appendix A, cover almost everything the working sci-
entist needs to know in order to get started in using Python effectively. My editor and
some referees suggested that I should devote the second half of the book to problems in
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:36, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.002
xiv Preface to the First Edition
a particular field. This would have led to a series of books, “Python for Biochemists”,
“Python for Crystallographers”, . . . , all with a common first half. Instead I have cho-
sen to cover just three topics, which, however, should be far more widely applicable in
many different fields. Chapter 8 covers four radically different types of ordinary differ-
ential equations and shows how to use the various relevant black boxes, which are often
Python wrappers around tried and trusted Fortran codes. The next chapter while os-
tensibly about pseudospectral approaches to evolutionary partial differential equations,
actually covers a topic of great utility to many scientists, namely how to reuse legacy
code, usually written in Fortran77, within Python at Fortran-like speeds, without under-
standing Fortran. The final chapter about solving very large linear systems via multigrid
is also a case history in how to use object-oriented programming meaningfully in a sci-
entific context. If readers look carefully and critically at these later chapters, they should
gain the practical expertise to handle problems in their own field.
Acknowledgments are due to the many Python developers who have produced and
documented a very useful tool, and also to the very many who have published code
snippets on the web, a great aid to the tyro, such as this author. Many of my colleagues
have offered valuable advice. Des Higham generously consented to my borrowing his
ideas for the last quarter of Chapter 8. I am especially grateful to Oliver Rinne who read
carefully and critically an early draft. At Cambridge University Press, my Production
Editor, Jessica Murphy and my Copy Editor, Anne Rix have exhibited their customary
expertise. Last but not least I thank the Department of Applied Mathematics and Theo-
retical Physics, Cambridge for continuing to offer me office space after my retirement,
which has facilitated the production of this book.
Writing a serious book is not a trivial task and so I am rather more than deeply
grateful for the near-infinite patience of Mary, the “Python-widow”, which made this
book possible!
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:36, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.002
1 Introduction
The title of this book is “Python for Scientists”, but what does that mean? The dictio-
nary defines “Python” as either (a) a non-venomous snake from Asia or Saharan Africa
or (b) a computer scripting language, and it is the second option which is intended here.
(What exactly this second definition means will be explained later.) By “scientist”, I
mean anyone who uses quantitative models either to obtain conclusions by processing
pre-collected experimental data or to model potentially observable results from a more
abstract theory, and who asks “what if?”. What if I analyse the data in a different way?
What if I change the model? Thus the term also includes economists, engineers and
mathematicians among others, as well as the usual concept of scientists. Given the vol-
ume of potential data or the complexity (non-linearity) of many theoretical models, the
use of computers to answer these questions is fast becoming mandatory.
Advances in computer hardware mean that immense amounts of data or ever more
complex models can be processed at increasingly rapid speeds. These advances also
mean reduced costs so that today virtually every scientist has access to a “personal
computer”, either a desktop work station or a laptop, and the distinction between the
two is narrowing quickly. It might seem to be a given that suitable software will also be
available so that the “what if” questions can be answered readily. However, this turns
out not always to be the case. A quick pragmatic reason is that, while there is a huge
market for hardware improvements, scientists form a very small fraction of it and so
there is little financial incentive to improve scientific software. But for scientists, this
issue is important and we need to examine it in more detail.
Before we discuss what is available, it is important to note that all computer software
comes in one of two types: proprietary and open-source. The first is supplied by a com-
mercial firm. Such organizations have both to pay wages and taxes and to provide a re-
turn for their shareholders. Therefore, they have to charge real money for their products,
and, in order to protect their assets from their competitors, they do not tell the customer
how their software works. Thus, the end-users have little chance of being able to adapt
or optimize the product for their own use. Since wages and taxes are recurrent expendi-
tures, the company needs to issue frequent charged-for updates and improvements (the
Danegeld effect). Open-source software is available for free or at nominal cost (media,
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.003
2 Introduction
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.003
1.1 Scientific Software 3
algebraic and analytic processes, and to integrate all of them with their numerical and
graphical properties. A disadvantage of all of these packages is the quirky syntax and
limited expressive ability of their command languages. Unlike the compiled languages,
it is often extremely difficult to program a process which was not envisaged by the
package authors.
The best of the proprietary packages are very easy to use with extensive on-line help
and coherent documentation, which has not yet been matched by all of the open-source
alternatives. However, a major downside of the commercial packages is the extremely
high prices charged for their licences. Most of them offer a cut down “student version”
at reduced price (but usable only while the student is in full-time education) so as to
encourage familiarity with the package. This largesse is paid for by other users.
Let us summarize the position. On the one hand, we have the traditional compiled
languages for numerics which are very general, very fast, very difficult to learn and do
not interact readily with graphical or algebraic processes. On the other, we have standard
scientific packages which are good at integrating numerics, algebra and graphics, but are
slow and limited in scope.
What properties should an ideal scientific package have? A short list might contain:
1. a mature programming language which is both easy to understand and which has
extensive expressive ability,
2. integration of algebraic, numerical and graphical functions,
3. the ability to generate numerical algorithms running with speeds within an order of
magnitude of the fastest of those generated by compiled languages,
4. a user interface with adequate on-line help, and decent documentation,
5. an extensive range of textbooks from which the curious reader can develop greater
understanding of the concepts,
6. open-source software, freely available,
7. implementation on all standard platforms, e.g., Linux, Mac OS X, Unix, Windows,
8. a concise package, and so implementable on even modest hardware.
The bad news is that no single “scientific package” quite satisfies all of these criteria.
Consider, e.g., the requirement of algebraic capability. There are two mature open-
source packages, wx-Maxima and Reduce, with significant algebraic capabilities worthy
of consideration, but Reduce fails requirement 4 and both fail criteria 3 and 5. They are,
however, extremely powerful tools in the hands of experienced users. Python, via the
add-on SymPy, see Chapter 7, almost achieves a high standard of algebraic capability.
SageMath fulfils all but the last of the criteria listed above. It is completely based on
Python and its add-ons, and also includes wx-Maxima. For further details see Chapter
7. Thus a rational strategy is to first master Python. If its, admirably few, weaknesses are
crucial for your work, then investigate SageMath. The vast majority of scientists will
find plenty of utility in Python.
In 1991, Guido van Rossum created Python as an open-source platform-independent
general purpose programming language. It is basically a very simple language sur-
rounded by an enormous library of add-on modules, including complete access to the
underlying operating system. This means that it can manage and manipulate programs
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.003
4 Introduction
built from other complete (even compiled) packages, i.e., it is a scripting language. This
versatility has ensured both its adoption by power users such as Google, and a real army
of developers. It means also that it can be a very powerful tool for the scientist. Of
course, there are other scripting languages, e.g., Java™ and Perl®, but none has the
versatility or user-base to meet criteria 3–5 above.
Ten years ago it would not have been possible to recommend Python for scientific
work. The size of the army of developers meant that there were several mutually incom-
patible add-on packages for numerical and scientific applications. Fortunately, reason
has prevailed and there is now a single numerical add-on package, NumPy, and a single
scientific one, SciPy, around which the developers have united. When the first edition
of this book was written SymPy, the Python approach to algebraic manipulation, was
still in a phase of rapid development, and so it was not included. While SymPy has yet
to achieve the capabilities of wx-Maxima and Reduce, it now handles many algebraic
tasks reliably
The purpose of this intentionally short book is to show how easy it is for the working
scientist to implement and test non-trivial mathematical algorithms using Python. We
have quite deliberately preferred brevity and simplicity to encyclopaedic coverage in
order to get the inquisitive reader up and running as soon as possible. We aim to leave
the reader with a well-founded framework to handle many basic, and not so basic, tasks.
Obviously, most readers will need to dig further into techniques for their particular
research needs. But after reading this book, they should have a sound basis for this.
This chapter and Appendix A discuss how to set up a scientific Python environment.
While the original Python interpreter was pretty basic, its replacement IPython is so
easy to use, powerful and versatile that Chapter 2 is devoted to it, adopting a hands-on
approach.
We now describe the subsequent chapters. As each new feature is described, we try
to illustrate it first by essentially trivial examples and, where appropriate, by more ex-
tended problems. This author cannot know the mathematical sophistication of potential
readers, but in later chapters we shall presume some familiarity with basic calculus,
e.g., the Taylor series in one dimension. However, for these extended problems we shall
sketch the background needed to understand them, and suitable references for further
reading will be given.
Chapter 3 gives a brief but reasonably comprehensive survey of those aspects of the
core Python language likely to be of most interest to scientists. Python is an object-
oriented language, which lends itself naturally to object-oriented programming (OOP),
which may well be unfamiliar to most scientists. We shall adopt an extremely light
touch to this topic. We should perhaps point out that the container objects introduced
in Section 3.5 do not all have precise analogues in, say, C or Fortran. Again the brief
introduction to Python classes in Section 3.9 may be unfamiliar to users of those two
families of languages. The chapter concludes with two implementations of the sieve
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.003
1.2 The Plan of This Book 5
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.003
6 Introduction
These first chapters cover the basic tools that Python provides to enhance the scien-
tist’s computer experience. How should we proceed further?
A notable omission is that apart, from a brief discussion in Section 4.5, the vast
subject of data analysis will not be covered. There are three main reasons for this.
(a) Recently an add-on module called Pandas has appeared. This uses NumPy and Mat-
plotlib to tackle precisely this issue. It comes with comprehensive documentation,
which is described in Section 4.5.
(b) One of the authors of Pandas has written a book, McKinney (2012), which reviews
IPython, NumPy and Matplotlib and goes on to treat Pandas applications in great
detail.
(c) I do not work in this area, and so would simply have to paraphrase the sources
above.
Instead, I have chosen to concentrate on the modelling activities of scientists. One
approach would be to target problems in bioinformatics or cosmology or crystallogra-
phy or engineering or epidemiology or financial mathematics or . . . etc. Indeed, a whole
series of books with a common first half could be produced called “Python for Bioin-
formatics” etc. A less profligate and potentially more useful approach would be to write
a second half applicable to all of these fields, and many more. I am relying here on the
unity of mathematics. Problems in one field when reduced to a core dimensionless form
often look like a similarly reduced problem from another field.
This property can be illustrated by the following example. In population dynamics,
we might study a single species whose population N(T ) depends on time T . Given a
plentiful food supply, we might expect exponential growth, dN/dT = kN(T ), where the
growth constant k has dimension 1/time. However, there are usually constraints limiting
such growth. A simple model to include these is the “logistic equation”
dN
(T ) = kN(T ) (N0 − N(T )) (1.1)
dT
which allows for a stable constant population N(T ) = N0 . The biological background to
this equation is discussed in many textbooks, e.g., Murray (2002).
In (homogeneous spherically symmetric) cosmology, the density parameter Ω de-
pends on the scale factor a via
dΩ (1 + 3w)
= Ω(1 − Ω), (1.2)
da a
where w is usually taken to be a constant.
Now mathematical biology and cosmology do not have a great deal in common, but
it is easy to see that (1.1) and (1.2) represent the same equation. Suppose we scale the
independent variable T in (1.1) by t = kN0 T , which renders the new time coordinate
t dimensionless. Similarly, we introduce the dimensionless variable x = N/N0 so that
(1.1) becomes the logistic equation
dx
= x(1 − x). (1.3)
dt
In a general relativistic theory, there is no reason to prefer any one time coordinate to
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.003
1.2 The Plan of This Book 7
any other. Thus we may choose a new time coordinate t via a = et/(1+3w) , and then,
on setting x = Ω, we see that (1.2) also reduces to (1.3). Thus the same equations
can arise in a number of different fields.3 In Chapters 8–10, we have, for brevity and
simplicity, used minimal equations such as (1.3). If the minimal form for your problem
looks something like the one being treated in a code snippet, you can of course hack the
snippet to handle the original long form for your problem.
Chapter 8 looks at four types of problems involving ordinary differential equations.
We start with a very brief introduction to techniques for solving initial value problems
and then look at a number of examples, including two classic non-linear problems, the
van der Pol oscillator and the Lorenz equations. Next we survey two-point boundary
value problems and examine both a linear Sturm–Liouville eigenvalue problem and an
exercise in continuation for the non-linear Bratu problem. Problems involving delay dif-
ferential equations arise frequently in control theory and in mathematical biology, e.g.,
the logistic and Mackey–Glass equations, and a discussion of their numerical solution
is given in the next section. Finally in this chapter we look briefly at stochastic calcu-
lus and stochastic ordinary differential equations. In particular, we consider a simple
example closely linked to the Black–Scholes equation of financial mathematics.
There are two other major Python topics relevant to scientists that I would like to
introduce here. The first is the incorporation of code written in other languages. There
are two aspects of this: (a) the reuse of pre-existing legacy code, usually written in
Fortran, (b) if one’s code is being slowed down seriously by a few Python functions, as
revealed by the profiler, how do we recode the offending functions in Fortran or C? The
second topic is how can a scientific user make worthwhile use of the object-oriented
programming (OOP) features of Python?
Chapter 9 addresses the first topic via an extended example. We look first at how
pseudospectral methods can be used to attack a large number of evolution problems
governed by partial differential equations, either initial value or initial-boundary value
problems. For the sake of brevity, we look only at problems with one time and one
spatial dimension. Here, as we explain, problems with periodic spatial dependence can
be handled very efficiently using Fourier methods, but for problems which are more
general, the use of Chebyshev transforms is desirable. However, in this case there is
no satisfactory Python black box available. It turns out that the necessary tools have
already been written in legacy Fortran77 code. These are listed in Appendix B, and we
show how, with an absolutely minimal knowledge of Fortran77, we can construct ex-
tremely fast Python functions to accomplish the required tasks. Our approach relies on
the NumPy f2py tool which is included in all of the recommended Python distributions.
If you are interested in possibly reusing pre-existing legacy code, it is worthwhile study-
ing this chapter even if the specific example treated there is not the task that you have
in mind. See also Section 1.3 for other uses for f2py.
One of the most useful features of object-oriented programming (OOP) from the
point of view of the scientist is the concept of classes. Classes exist in C++ (but not
3 This example was chosen as a pedagogic example. If the initial value x(0) = x0 is specified, then the exact
solution is x(t) = x0 /[x0 + (1 − x0 )e−t ]. In the current context, x0 0. If x0 1, then all solutions tend
monotonically towards the constant solution x = 1 as t increases. See also Section 8.5.3.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.003
8 Introduction
C) and Fortran90 and later (but not Fortran77). However, both implementations are
complicated and so are usually shunned by novice programmers. In contrast, Python’s
implementation is much simpler and more user-friendly, at the cost of omitting some of
the more arcane features of other language implementations. We give a very brief intro-
duction to the syntax in Section 3.9. However, in Chapter 10 we present a much more
realistic example: the use of multigrid to solve elliptic partial differential equations in
an arbitrary number of dimensions, although for brevity the example code is for two di-
mensions. Multigrid is by now a classical problem which is best defined recursively, and
we devote a few pages to describing it, at least in outline. The pre-existing legacy code
is quite complicated because the authors needed to simulate recursion in languages,
e.g., Fortran77, which do not support recursion. Of course, we could implement this
code using the f2py tool outlined in Chapter 9. Instead, we have chosen to use Python
classes and recursion to construct a simple clear multigrid code. As a concrete example,
we use the sample problem from the corresponding chapter in Press et al. (2007) so
that the inquisitive reader can compare the non-recursive and OOP approaches. If you
have no particular interest in multigrid, but do have problems involving linked math-
ematical structures, and such problems arise often in, e.g., bioinformatics, chemistry,
epidemiology and solid-state physics among others, then you should certainly peruse
this final chapter to see how, if you state reasonably mathematically precisely what your
problems are, then it is easy to construct Python code to solve them.
The most common criticism of Python and the scientific software packages is that they
are far too slow, in comparison with compiled code, when handling complicated realistic
problems. The speed-hungry reader might like to look at a recent study4 of a straight-
forward “number-crunching” problem treated by various methods. Although the figures
given in the final section refer to one particular problem treated on a single processor,
they do give a “ball park” impression of performance. As a benchmark, they use the
speed of a fully compiled C++ program which solves the problem. A Python solution
using the technique of Chapter 3, i.e., core Python, is about 700 times slower. Once
you use the floating-point module NumPy and the techniques described in Chapter 4
the code is only about ten times slower, and the Matlab performance is estimated to be
similar. However, as the study indicates, there are a number of ways to speed up Python
to about 80% of the C++ performance. Some of these are very rewarding exercises in
computer science.
One in particular, though, is extremely useful for scientists: the f2py tool. This is
discussed in detail in Chapter 9, where we show how we can reuse legacy Fortran code.
It can also be used to access standard Fortran libraries, e.g., the NAG libraries.5 Yet
another use is to speed up NumPy code and so improve performance! To see how this
works, suppose we have developed a program such as those outlined in the later sections
4 See http://wiki.scipy.org/PerformancePython.
5 See, e.g., http://www.nag.co.uk/doc/TechRep/pdf/TR1_08.pdf.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.003
1.4 Limitations of This Book 9
of the book, which uses a large number of functions, each of which carries out a simple
task. The program works correctly, but is unacceptably slow. Note that getting detailed
timing data for Python code is straightforward. Python includes a “profiler” which can
be run on the working program. This outputs a detailed list of the functions ordered by
the time spent executing them. It is very easy to use, and this is described in Section 2.5.
Usually, there are one or two functions which take a very long time to execute simple
algorithms.
This is where f2py comes into its own. Because the functions are simple, even begin-
ners can soon create equivalent code in, say, Fortran77 or Ansi C. Also, because what
we are coding is simple, there is no need for the elaborate (and laborious to learn) fea-
tures of, say, Fortran95 or C++. Next we encapsulate the code in Python functions using
the f2py tool, and slot them into the Python program. With a little experience, we can
achieve speeds comparable to that of a program written fully in, say, Fortran95.
A comprehensive treatment of Python and its various branches would occupy several
large volumes and would be out of date before it reached the bookshops. This book
is intended to offer the reader a starting point which is sufficient to be able to use the
fundamental add-on packages. Once the reader has a little experience with what Python
can do, it is time to explore further those areas which interest the reader.
I am conscious of the fact that I have not even mentioned vitally important concepts,
e.g., finite-volume methods for hyperbolic problems,6 parallel programming and real-
time graphics to name but a few areas in which Python is very useful. There is a very
large army of Python developers working at the frontiers of research, and their endeav-
ours are readily accessed via the internet. Please think of this little book as a transport
facility towards the front line.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.003
10 Introduction
pursue the pundits’ policy. Please save your energy, sanity etc., and read Appendix A,
which I have quite deliberately targeted at novices, for the obvious reason!
Admittedly, there is an amount, albeit slight and low-level, of hassle involved here.
So what’s the payoff? Well, if you follow the routes suggested in Appendix A, you
should end up with a system which works seamlessly. While it is true that the original
Python interpreter was not terribly user-friendly, which caused all of the established IDE
purveyors to offer a “Python mode”, the need which they purported to supply has been
overtaken by the enhanced interpreter IPython. Indeed, in its latest versions IPython
hopes to surpass the facilities offered by Matlab, Mathematica and the Python-related
features of commercial IDEs. In particular, it allows you to use your favourite editor,
not theirs, and to tailor its commands to your needs, as explained in Appendix A and
Chapter 2.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.003
2 Getting Started with IPython
This sounds like software produced by Apple®, but it is in fact a Python interpreter on
steroids. It has been designed and written by scientists with the aim of offering very fast
exploration and construction of code with minimal typing effort, and offering appro-
priate, even maximal, on-screen help when required. Documentation and much more is
available on the website.1 This chapter is a brief introduction to the essentials of using
IPython. A more extended discursive treatment can be found in, e.g., Rossant (2015).
In this chapter we shall concentrate on notebook and terminal modes, and we assume
that the reader has set up the environments as described in Sections A.2 and A.3. Before
we get to realistic examples, I must ask for the impatient reader’s forbearance. Tab com-
pletion, Section 2.1, is an unusual but effective method for minimizing key-strokes, and
the introspection feature, Section 2.2 shows how to generate relevant inline information
quickly, without pausing to consult the manual.
While using the IPython interpreter, tab completion is always present. This means that,
whenever we start typing a Python-related name on a line or in a cell, we can pause
and press the tab key, to see a list of names valid in this context, which agree with the
characters already typed.
As an example, suppose we need to type import matplotlib.2 Typing itab re-
veals 15 possible completions. By inspection, only one of them has second letter m, so
that imtab will complete to import. Augmenting this to import mtab shows 30 pos-
sibilities, and by inspection we see that we need to complete the command by import
matptab to complete the desired line.
That example was somewhat contrived. Here is a more compulsive reason for using
tab completion. When developing code, we tend, lazily, to use short names for vari-
ables, functions etc. (In early versions of Fortran, we were indeed restricted to six or
eight characters, but nowadays the length can be arbitrary.) Short names are not always
meaningful ones, and the danger is that if we revisit the code in six months, the intent of
the code may no longer be self-evident. By using meaningful names of whatever length
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.004
12 Getting Started with IPython
is needed, we can avoid this trap. Because of tab completion, the full long name only
need be typed once.
2.2 Introspection
IPython has the ability to inspect just about any Python construct, including itself, and
to report whatever information its developers have chosen to make available. This fa-
cility is called introspection. It is accessed by the single character ?. The easiest way to
understand it is to use it, so you are recommended to fire up the interpreter.
Which mode, terminal or notebook, should one use? Beginners should start with ter-
minal mode, as described in Section A.3, thus removing one level of complexity. This
persists throughout this chapter until Section 2.5, where the code snippets are extremely
short. If you choose to use terminal mode, type ipython followed by a ret on the com-
mand line. IPython will respond with quite a lengthy header, followed by an input line
labelled In [1]:. Now that you are in IPython, you can try out introspection by typing
? (followed by ret) on the input line. (Note that within IPython terminal mode the ret
actually executes the command in the current line.) IPython responds by issuing in pager
mode3 a summary of all of the facilities available. If you exit this, then the command
quickref (hint: use tab completion) gives a more concise version. Very careful study
of both documents is highly recommended.
IPython notebook users need to use slightly different commands. After invoking the
notebook, see Section A.2.2 for details, they will be confronted with an unnumbered
single-line blank cell. Now they can try out introspection by typing ? on the input line.
Typing ret at this point merely adds a new line to the cell. In order to execute the
command(s) in the cell one needs to add either shift+ret (which also creates a new cell
below the current one), or ctl+ret which just executes the command(s). The output, the
lengthy facilities summary, appears in a scrollable window at the bottom of the screen.
It can be killed by clicking on the x-button at the top right of the window. The command
quickref (hint: use tab completion) followed by ctl-ret gives a more concise version.
Again very careful study of both documents is highly recommended.
However, scientists are impatient folk, and the purpose of this chapter is to get them
up and running with the most useful features. Therefore, we need to type in some Python
code, which newcomers will have to take on trust until they have mastered Chapters 3
and 4. Again, depending on whether you are operating in notebook or console mode,
the procedures differ slightly.
Notebook users should type each boxed line or boxed code snippet into a cell. They
can then execute the code via ctl+ret or shift+ret. Readers using terminal mode
3 This is based on the Unix less facility. To use it effectively you need know only four commands:
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.004
2.2 Introspection 13
should type in, line by line, boxed lines or code snippets and enter each line separately
using the ret key.
For example, please type in
a=3
b=2.7
c=12 + 5j
s=’Hello World!’
L=[a, b, c, s, 77.77]
4 Note that in the code snippet there is no multiplication sign (*) between 5 and j.
5 For the sake of brevity, we shall not distinguish between a line in terminal mode and a one-line cell in
notebook mode.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.004
14 Getting Started with IPython
2.3 History
If you look at the output from the code in the previous section, you will see that IPython
employs a history mechanism which is very similar to that in Mathematica notebooks.
Input lines are labelled In[1], In[2], . . . , and if input In[n] produces any output, it
is labelled Out[n]. As a convenience, the past three input lines/cells are available as
_i, _ii and _iii, and the corresponding output lines/cells are available as _, __ and
___. In practice though, you can insert the content of a previous input line/cell into the
current one by navigating using ↑ (or ctl-p) and ↓ (or ctl-n), and this is perhaps the
most common usage. Unusually, but conveniently, history persists in terminal mode. If
you close down IPython (using exit) and then restart it, the history of the previous
session is still available via the arrow keys. There are many more elaborate things you
can do with the history mechanism, try the command history?.
The IPython interpreter expects to receive valid Python commands. However, it is very
convenient to be able to type commands which control either the behaviour of IPython
or that of the underlying operating system. Such commands, which coexist with Python
ones, are called magic commands. A very long very detailed description can be found
by typing %magic in the interpreter, and a compact list of available commands is given
by typing %lsmagic. (Do not forget tab completion!) Note that there are two types of
magic, line magic, prefixed by %, and cell magic, prefixed by %%. The latter is relevant
only to notebook mode. You can get very helpful documentation on each command by
using introspection.
Let us start by considering system commands. A harmless example is pwd, which
comes from the Unix operating system where it just prints the name of the current
directory (print working directory) and exits. There are usually three ways to achieve
this in the IPython window. You should try out the following commands.
!pwd
Nothing in Python starts with a “!”, and IPython interprets this as the Unix shell com-
mand pwd, and produces the ASCII answer.
%pwd
Nothing in Python starts with a “%”, and IPython treats this as a line magic command,
interpreting it as the shell command. The u indicates that the string is encoded in Uni-
code, which enables a rich variety of outputs. Unicode is mentioned briefly in Section
A.3.1.
pwd
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.004
2.5 IPython in Action: An Extended Example 15
Here is a subtle, but useful, feature. For line magic commands, the % prefix is not al-
ways necessary, see the discussion of %automagic below. The interpreter sees no other
meaning for pwd and so treats it as %pwd.
pwd=’hoho’
pwd
pwd
Now that pwd has no other assignment, it works as a line magic command.
%automagic?
If %automagic is on, as it is by default, the single per cent sign (%) which starts all line
magic commands, can be omitted. This is a great convenience, provided one remembers
that one is dealing with a magic command.
Magic cell commands start with two mandatory per cent signs. They operate on entire
cells, and can be extremely useful, see, e.g., the next section.
For the rest of this chapter we present the first part of an extended example, in order to
show the effectiveness of magic commands. The second part is the example, in Section
3.9, where we consider how to implement arbitrary precision real arithmetic via frac-
tions. There is an issue with fractions, e.g., 3/7 and 24/56 are usually regarded as the
same number. Thus there is a problem here, to determine the “highest common factor”
of two integers a and b, or, as mathematicians are wont to say, their “greatest common
divisor” (GCD), which can be used to produce a canonical form for the fraction a/b.
(By inspection of factors, the GCD of 24 and 56 is 8, which implies 24/56 = 3/7 and no
further reduction of the latter is possible.) Inspection of factors is not easily automated,
and a little research, e.g., on the web, reveals Euclid’s algorithm. To express this con-
cisely, we need a piece of jargon. Suppose a and b are integers. Consider long division
of a by b. The remainder is called a mod b, e.g., 13 mod 5 = 3, and 5 mod 13 = 5. Now
denote the GCD of a and b by gcd(a, b). Euclid’s algorithm is most easily described
recursively via
gcd(a, 0) = a, gcd(a, b) = gcd(b, a mod b), (b 0). (2.1)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.004
16 Getting Started with IPython
Try evaluating by hand gcd(56, 24) according to this recipe. It’s very fast! It can be
shown that the most laborious case arises when a and b are consecutive Fibonacci num-
bers, so they would be useful for a test case. The Fibonacci numbers Fn are defined
recursively via
F0 = 0, F1 = 1, Fn = Fn−1 + Fn−2 , n 2. (2.2)
The sequence begins 0, 1, 1, 2, 3, 5, 8, . . ..
How do we implement both Euclid’s algorithm and Fibonacci numbers efficiently
and speedily in Python? We start with the Fibonacci task because it looks to be more
straightforward.
In order to get started with mastering IPython, the novice reader is asked to take
on trust the next two code snippets. A partial explanation is offered here, but all of
the features will be explained more thoroughly in Chapter 3. However, an important
point needs to be made here. Every programming language needs subsidiary blocks of
code, e.g., the contents of functions or do-loops. Most delineate them by some form of
bracketing, but Python relies exclusively on indentation. All blocks of code must have
the same indentation. The need for a sub-block is usually indicated by a colon (:). A
sub-block is further indented (four spaces is the conventional choice), and the end of
the sub-block is indicated by the cancellation of this extra indentation. IPython and all
Python-aware editors will handle this automatically. Three examples are given below.
The novice reader should try to understand, perhaps roughly, what is going on, before
we move on to consider possible workflows to execute the snippets.
1 # File: fib.py Fibonacci numbers
2
7 def fib(n):
8 """ Returns n’th Fibonacci number. """
9 a,b=0,1
10 for i in range(n):
11 a,b=b,a+b
12 return a
13
14 ####################################################
15 if __name__ == "__main__":
16 for i in range(1001):
17 print "fib(",i,") = ",fib(i)
The details of Python syntax are explained in Chapter 3. For the time being, note that
lines starting with a hash (#), e.g., lines 1 and 14, denote comments. Also lines 3–5
define a docstring, whose purpose will be explained shortly. Lines 7–12 define a Python
function. Note the point made above that every colon (:) demands an indentation. Line 7
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.004
2.5 IPython in Action: An Extended Example 17
is the function declaration. Line 8 is the function docstring, again soon to be explained.
Line 9 introduces identifiers a and b, which are local to this function, and refer initially
to the values 0 and 1 respectively. Next examine line 11, ignoring for the moment its in-
dentation. Here a is set to refer to the value that b originally referred to. Simultaneously,
b is set to refer to the sum of values originally referred to by a and b. Clearly, lines 9
and 11 replicate the calculations implicit in (2.2). Now line 10 introduces a for-loop or
do-loop, explained in Section 3.7.1, which extends over line 11. Here range(n) gener-
ates a dummy list with n elements, [0, 1, . . . , n − 1], and so line 11 is executed precisely
n times. Finally, line 12 exits the function with the return value set to that referred to
finally by a.
Naturally, we need to provide a test suite to demonstrate that this function behaves as
intended. Line 14 is simply a comment. Line 15 will be explained soon. (When typing it,
note that there are four pairs of underscores.) Because it is an if statement terminated by
a colon, all subsequent lines need to be indented. We have already seen the idea behind
line 16. We repeat line 17 precisely 1001 times with i = 0, 1, 2, . . . , 1000. It prints a
string with four characters, the value of i, another string with four characters, and the
value of fib(i).
We now present two possible workflows for creating and using this snippet.
Now run the cell again. This write the cell content to the file fib.py in the current
directory, or overwrites that file if it already existed.
Once the program has been verified, we can ask how fast is it? Run the program again
but with the enhanced magic command run -t fib and IPython will produce timing
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.004
18 Getting Started with IPython
data. On my machine, the “User time” is 0.05 s, but the “Wall time” is 0.6 s. Clearly,
the discrepancy reflects the very large number of characters printed to the screen. To
verify this, modify the snippet as follows. Comment out the print statement in line 17 by
inserting a hash (#) character at the start of the line. Add a new line 18: fib(i), being
careful to get the indentation correct. (This evaluates the function, but does nothing with
the value.) Now run the program again. On my machine it takes 0.03 s, showing that
fib(i) is fast, but printing is not. (Don’t forget to comment out line 18, and uncomment
in line 17!)
We still need to explain the docstrings in lines 3–5 and 8, and the weird line 15. Close
down IPython (in terminal mode, use exit) and then reopen a fresh version. Type the
single line import fib, which reflects the core of the filename. The tail .py is not
needed. We have imported an object fib. What is it? Introspection suggests the com-
mand fib?, and IPython’s response is to print the docstring from lines 3–5 of the snip-
pet. This suggests that we find out more about the function fib.fib, so try fib.fib?,
and we are returned the docstring from line 8. The purpose of docstrings, which are
messages enclosed in pairs of triple double-quotes, is to offer online documentation to
other users and, just as importantly, you in a few days time! However, introspection has
a further trick up its sleeve. Try fib.fib?? and you will receive a listing of the source
code for this function!
You should have noticed that import fib did not list the first 1001 Fibonacci num-
bers. Had we instead, in a separate session, issued the command run fib, they would
have been printed! Line 15 of the snippet detects whether the file fib.py is being im-
ported or run, and responds accordingly without or with the test suite. How it does this
is explained in Section 3.4.
Now we return to our original task, which was to implement the gcd function implicit
in equation (2.1). Once we recognize that (i) Python has no problem with recursion, and
(ii) a mod b is implemented as a%b, then a minimal thought solution suggests itself, as
in the following snippet. (Ignore for the time being lines 14–18.)
7 def gcdr(a,b):
8 """ Euclidean algorithm, recursive vers., returns GCD. """
9 if b==0:
10 return a
11 else:
12 return gcdr(b,a%b)
13
14 def gcd(a,b):
15 """ Euclidean algorithm, non-recursive vers., returns GCD. """
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.004
2.5 IPython in Action: An Extended Example 19
16 while b:
17 a,b=b,a%b
18 return a
19
20 ##########################################################
21 if __name__ == "__main__":
22 import fib
23
24 for i in range(963):
25 print i, ’ ’, gcd(fib.fib(i),fib.fib(i+1))
The only real novelty in this snippet is the import fib statement in line 22, and we
have already discussed its effect above. The number of times the loop in lines 24 and 25
is executed is crucial. As printed, this snippet should run in a fraction of a second. Now
change the parameter 963 in line 24 to 964, save the file, and apply run gcd again. You
should find that the output appears to be in an infinite loop, but be patient. Eventually,
the process will terminate with an error statement that the maximum recursion depth
has been exceeded. While Python allows recursion, there is a limit on the number of
self-calls that can be made.
This limitation may or may not be a problem for you. But it is worth a few moments
thought to decide whether we could implement Euclid’s algorithm (2.1) without using
recursion. I offer one possible solution in the function gcd implemented in lines 14–18
of the snippet. Lines 16 and 17 define a while loop, note the colon terminating line 16.
Between while and the colon, Python expects an expression which evaluates to one of
the Boolean values True or False. As long as True is found, the loop executes line 17,
and then retests the expression. If the test produces False, then the loop terminates
and control passes to the next statement, line 18. In the expected context, b will always
be an integer, so how can an integer take a Boolean value? The answer is remarkably
simple. The integer value zero is always coerced to False, but all non-zero values
coerce to True. Thus the loop terminates when b becomes zero, and then the function
returns the value a. This is the first clause of (2.1). The transformation in line 17 is the
second clause, so this function implements the algorithm. It is shorter than the recursive
function, can be called an arbitrary number of times and, as we shall see, runs faster.
So was the expenditure of thought worthwhile? Using the run command, we can
obtain revealing statistics. First, edit the snippet to make 963 loops with the gcdr func-
tion, and save it. Now invoke run -t gcd to obtain the time spent. On my machine, the
“User time” is 0.31 s. Yours will vary, but it is relative timings that matter. The “Wall
time” reflects the display overhead and is not relevant here. Next invoke run -p gcd,
which invokes the Python profiler. Although you would need to read the documenta-
tion to understand every facet of the resulting display, a little scientific intuition can be
very useful. This shows that there were 963 direct calls (as expected) of the function
gcdr, within a total of 464,167 actual calls. The actual time spent within this func-
tion was 0.237 s. Next there were 1926 calls (as expected) of the function fib, and the
time expended was 0.084 s. Note that these timings cannot be compared with the older
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.004
20 Getting Started with IPython
run -t gcd ones, because the newer ones include the profiler overhead, which is sig-
nificant here. However, we can conclude that about 74% of the time was spent in the
function gcdr.
Next we need to repeat the exercise for the gcd function. Amend line 25 of the snippet
to replace gcdr by gcd and resave the file. Now run -t gcd to get a “User time” of
0.20 s. The other command run -p gcd reveals that the 1926 calls of function fib
took 0.090 s. However, the function gcd was called only 993 times (as expected), which
occupied 0.087 s. Thus gcd occupied about 49% of the time taken. Very approximately,
these relative timings factor out the profiler overhead. Now 74% of the 0.31 s timing
for the recursive version is 0.23 s, while 49% of the 0.20 s time for the non-recursive
version is 0.098 s. Thus the expenditure of thought has produced a shortened code which
runs in 43% of the time of the “thoughtless code”!
There are two points which need to be gleaned from this example.
1. The IPython magic command run or %run is the Python workhorse. You do need,
via introspection, to study its docstring. Look also at the variants %run -t and
%run -p. It is also worthwhile introspecting %timeit at this stage.
2. You will see much in the literature about methods for “speeding up” Python. These
are often very clever pieces of software engineering. But none are as effective as
human ingenuity!
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.004
3 A Short Python Tutorial
Although Python is a small language, it is a very rich one. It is very tempting, when
writing a textbook, to spell out all of the ramifications, concept by concept. The obvious
example is the introductory tutorial from the originator of Python, Guido van Rossum.
This is available in electronic form as the tutorial in your Python documentation or
on-line1 or as hard copy (van Rossum and Drake Jr. (2011)). It is relatively terse at
150 printed pages, and does not mention NumPy. My favourite textbook, Lutz (2013),
runs to over 1500 pages, a marathon learning curve, and mentions NumPy only in pass-
ing. It is excellent at explaining the features in detail, but is too expansive for a first
course in Python. A similar criticism can be applied to two books with a more scientific
orientation, Langtangen (2009) and Langtangen (2014), both around 800 pages, with a
significant overlap between them. I recommend these various books among many others
for reference, but not for learning the language.
Very few people would learn a foreign language by first mastering a grammar text-
book and then memorizing a dictionary. Most start with a few rudiments of grammar
and a tiny vocabulary. Then by practice they gradually extend their range of constructs
and working vocabulary. This allows them to comprehend and speak the language very
quickly, and it is the approach to learning Python that is being adopted here. The dis-
advantage is that the grammar and vocabulary are diffused throughout the learning pro-
cess, but this is ameliorated by the existence of textbooks, such as those cited in the first
paragraph.
Although the narrative can simply be read, it is extremely helpful to have the IPython
terminal to hand, so that you can try out the code samples. For longer code snippets,
e.g., those in Sections 3.9 and 3.11, it is advisable to use either notebook mode, or
terminal mode together with an editor, so that you can save the code. Your choices are
described in Sections A.2 and A.3 of Appendix A. After trying out the code snippets,
you are strongly encouraged to try out your own experiments in the interpreter.
Every programming language includes blocks of code, which consist of one or more
lines of code forming a syntactic whole. Python uses rather fewer parentheses () and
braces {} than other languages, and instead uses indentation as a tool for formatting
1 It is available at http://docs.python.org/2/tutorial.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
22 A Short Python Tutorial
blocks. After any line ending in a colon, :, a block is required, and it is differentiated
from the surrounding code by being consistently indented. Although the amount is not
specified, the unofficial standard is four spaces. IPython and any Python-aware text
editor will do this automatically. To revert to the original indentation level, use the
ret key to enter a totally empty line. Removing braces improves readability, but the
disadvantage is that each line in a block must have the same indentation as the one
before, or a syntax error will occur.
Python allows two forms of comments. A hash symbol, #, indicates that the rest of
the current line is a comment, or more precisely a “tweet”. A “documentation string” or
docstring can run over many lines and include any printable character. It is delimited by
a pair of triple quotes, e.g.,
For completeness, we note that we may place several statements on the same line, pro-
vided we separate them with semicolons, but we should think about readability. Long
statements can be broken up with the continuation symbol ‘\’. More usefully, if a state-
ment includes a pair of brackets, (), we can split the line at any point between them
without the need for the continuation symbol. Here are simple examples.
Python deals exclusively with objects and identifiers. An object may be thought of as a
region of computer memory containing both some data and information associated with
those data. For a simple object, this information consists of its type and its identity,2 i.e.,
the location in memory, which is of course machine dependent. The identity is therefore
of no interest for most users. They need a machine-independent method for accessing
objects. This is provided by an identifier, a label which can be attached to objects. It is
made up of one or more characters. The first must be a letter or underscore, and any sub-
sequent characters must be digits, letters or underscores. Identifiers are case-sensitive:
x and X are different identifiers. (Identifiers which have leading and/or trailing under-
scores have specialized uses, and should be avoided by the beginner.) We must avoid
using predefined words, e.g., list, and should always try to use meaningful identifiers.
However, the choice among, say xnew, x_new and xNew is a matter of taste. Consider
2 An unfortunate choice of name, not to be confused with the about-to-be-defined identifiers.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
3.2 Objects and Identifiers 23
Figure 3.1 A schematic representation of assignments in Python. After the first command
p=3.14, the float object 3.14 is created and identifier p is assigned to it. Here the object is
depicted by its identity, a large number, its address in the memory of my computer (highly
machine-dependent) where the data are stored, and the type. The second command q=p assigns
identifier q to the same object. The third command p=’pi’ assigns p to a new “string” object,
leaving q pointing to the original float object.
the following code, which makes most sense if typed in, line-by-line, in the terminal
window.
1 p=3.14
2 p
3 q=p
4 p=’pi’
5 p
6 q
Note that we never declared the type of the object referred to by the identifier p. We
would have had to declare p to be of type “double” in C and “real*8” in Fortran. This is
no accident or oversight. A fundamental feature of Python is that the type belongs to
the object, not to the identifier.3
Next, in line 3, we set q=p. The right-hand side is replaced by whatever object p
pointed to, and q is a new identifier which points to this object, see Figure 3.1. No
equality of identifiers q and p is implied here! Notice that, in line 4, we reassign the
identifier p to a “string” object. However, the original float object is still pointed to by
the identifier q, see Figure 3.1, and this is confirmed by the output of lines 5 and 6. Sup-
pose we were to reassign the identifier q. Then, unless in the interim another identifier
had been assigned to q, the original “float” object would have no identifier assigned to
it and so becomes inaccessible to the programmer. Python will detect this automatically
and silently free up the computer memory, a process known as garbage collection.
3 The curious can find the type of an object with identifier p with the command type(p) and its identity
with id(p).
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
24 A Short Python Tutorial
Because of its importance in what follows we emphasize the point that the basic
building block in Python is the assignment operation, which despite appearances has
nothing to do with equality. In pseudocode,
<identifier>=<object>
which will appear over and over again. As we have already stated earlier, the type of
an object “belongs” to the object and not to any identifier assigned to it. Henceforth we
shall try to be less pedantic!
Since we have introduced a “float”, albeit informally, we turn next to a simple class
of objects.
3.3 Numbers
Python contains three simple types of number objects, and we introduce a fourth, not so
simple, one.
3.3.1 Integers
Python refers to integers as ints. Early versions supported integers only in the range
[−231 , 231 − 1], but in recent versions the range is considerably larger and is now limited
only by the availability of memory.
The usual operations of addition (+), subtraction (−) and multiplication (∗) are of
course available. There is a slight problem with division, for though p and q may be
integers, p/q need not. We may assume without loss of generality that q > 0, in which
case there exist unique integers m and n with
p = mq + n, where 0 n < q.
Then integer division in Python is defined by p//q, which returns m. The remainder n
is available as p%q. Exponentiation pq is also available as p**q, and can produce a real
number if q < 0.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
3.3 Numbers 25
is automatically widened to be a float. The same applies for division if one operand is
an int and the other is a float. However, if both are ints, e.g., ±1/5, what is the result?
Earlier versions (< 3.0) of Python adopt integer division, 1/5=0 and -1/5=-1, while
versions 3.0 use real division, 1/5=0.2 and -1/5=-0.2. This is a potential pitfall
which is easily avoided. Either use integer division // or widen one of the operands to
ensure an unambiguous result.
Python has a useful feature inherited from its C roots. Suppose we wish to increment
the float referred to by a by two. Clearly the code
temp=a+2
a=temp
will work. However, it is faster and more efficient to use the single instruction
a+=2
import math
dir(math) # or math.<TAB> in IPython
To find out more about the individual objects, we can either consult the written docu-
mentation or use the built-in help, e.g., in IPython
math.atan2? # or help(math.atan2)
If one is already familiar with the contents, then a quick-and-dirty-fix is to replace the
import command above by
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
26 A Short Python Tutorial
anywhere in the code before invoking the functions. Then the function mentioned above
is available as atan2(y,x) rather than math.atan2(y,x), which at first sight looks
appealing. However, there is a another module, cmath, which includes many standard
mathematical functions for complex numbers. Now suppose we repeat the quick-and-
dirty fix.
from cmath import *
Then what does atan2(y,x) refer to? It is unambiguous to widen a real to a complex
number, but not in the other direction! Note that, unlike C, the import command can
occur anywhere in the program before its contents are needed, so chaos is waiting,
imperturbably, to wreck your calculation! Of course Python knows about this, and the
recommended workflow is described in Section 3.4.
However, for the comparison operators “x>y”, “x>=y”, “x<y” and “x<=y”, widening
takes place if necessary. Unusually, but conveniently, chaining of comparison operators
is allowed, e.g., “0<=x<1<y>z” is equivalent to
(0<=x) and (x<1) and (1<y) and (y>z)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
3.4 Namespaces and Modules 27
While Python is running, it needs to keep a list of those identifiers which have been
assigned to objects. This list is called a namespace, and as a Python object it too has
an identifier. For example, while working in the interpreter, the namespace has the un-
memorable name __main__.
One of the strengths of Python is its ability to include files of objects, functions etc.,
written either by you or by someone else. To enable this inclusion, suppose you have
created a file containing objects, e.g., obj1, obj2 that you want to reuse. The file should
be saved as, e.g., foo.py, where the .py ending is mandatory. (Note that with most text
editors you need this ending for the editor to realize that it is dealing with Python code.)
This file is then called a module. The module’s identifier is foo, i.e., the filename minus
the ending.
This module can be imported into subsequent sessions via
import foo
(When the module is first imported, it is compiled into bytecode and written back to
storage as a file foo.pyc. On subsequent imports, the interpreter loads this precompiled
bytecode unless the modification date of foo.py is more recent, in which case a new
version of the file foo.pyc is generated automatically.)
One effect of this import is to make the namespace of the module available as foo.
Then the objects from foo are available with, e.g., identifiers foo.obj1 and foo.obj2.
If you are absolutely sure that obj1 and obj2 will not clash with identifiers in the
current namespace, you can import them via
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
28 A Short Python Tutorial
which imports everything from the module foo’s namespace. If an identifier obj1 al-
ready existed, its identifier will be overwritten by this import process, which usually
means that the object becomes inaccessible. For example, suppose we had an identifier
gamma referring to a float. Then
overwrites this and gamma now refers to the (real) gamma-function. A subsequent
overwrites gamma with the (complex) gamma-function! Note too that import statements
can appear anywhere in Python code, and so chaos is lurking if we use this option.
Except for quick, exploratory work in the interpreter, it is far better to modify the
import statements as, e.g.,
import math as re
import cmath as co
so that in the example above gamma, re.gamma and co.gamma are all available.
We now have sufficient background to explain the mysterious code line
if __name__ == "__main__"
which occurred in both of the snippets in Section 2.5. The first instance occurred in a
file fib.py. Now if we import this module into the interpreter, its name is fib and not
__main__ and so the lines after this code line will be ignored. However, when devel-
oping the functions in the module, it is normal to make the module available directly,
usually via the %run command. Then, as explained at the start of this section, the con-
tents are read into the __main__ namespace. Then the if condition of the code line is
satisfied and the subsequent lines will be executed. In practice, this is incredibly con-
venient. While developing a suite of objects, e.g., functions, we can keep the ancillary
test functions nearby. In production mode via import, these ancillary functions are ef-
fectively “commented out”.
The usefulness of computers is based in large part on their ability to carry out repeti-
tive tasks very quickly. Most programming languages therefore provide container ob-
jects, often called arrays, which can store large numbers of objects of the same type,
and retrieve them via an indexing mechanism. Mathematical vectors would correspond
to one-dimensional arrays, matrices to two-dimensional arrays etc. It may come as a
surprise to find that the Python core language has no array concept. Instead, it has con-
tainer objects which are much more general, lists, tuples, strings and dictionaries. It
will soon become clear that we can simulate an array object via a list, and this is how
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
3.5 Container Objects 29
numerical work in Python used to be done. Because of the generality of lists, such sim-
ulations took a great deal longer than equivalent constructions in Fortran or C, and this
gave Python a deservedly poor reputation for its slowness in numerical work. Devel-
opers produced various schemes to alleviate this, and they have now standardized on
the NumPy add-on module to be described in Chapter 4. Arrays in NumPy have much
of the versatility of Python lists, but are implemented behind the scenes as arrays in
C, significantly reducing, but not quite eliminating, the speed penalty. However, in this
section we describe the core container objects in sufficient detail for much scientific
work. They excel in the “administrative, bookkeeping chores” where Fortran and C are
at their weakest. Number-crunching numerical arrays are deferred to the next chapter,
but the reader particularly interested in numerics will need to understand the content of
this section, because the ideas developed here carry forward into the next chapter.
3.5.1 Lists
Consider typing the code snippet into the IPython terminal.
1 [1,4.0,’a’]
2 u=[1,4.0,’a’]
3 v=[3.14,2.78,u,42]
4 v
5 len(v)
6 len? # or help(len)
7 v*2
8 v+u
9 v.append(’foo’)
10 v
Line 1 is our first instance of a Python list, an ordered sequence of Python objects sepa-
rated by commas and surrounded by square brackets. It is itself a Python object, and can
be assigned to a Python identifier, as in line 2. Unlike arrays, there is no requirement that
the elements of a list be all of the same type. In lines 3 and 4, we see that in creating the
list an identifier is replaced by the object it refers to, e.g., one list can be an element in
another. The beginner should consult Figure 3.1 again. It is the object, not the identifier,
which matters. In line 5, we invoke an extremely useful Python function len() which
returns the length of the list, here 4. (Python functions will be discussed in Section 3.8.
In the meantime, we can find what len does by typing the line len? in IPython.) We
can replicate lists by constructions like line 7, and concatenate lists as in line 8. We can
append items to the ends of lists as in line 9. Here v.append() is another useful func-
tion, available only for lists. You should try v.append? or help(v.append) to see a
description of it. Incidentally, list. followed by tab completion or help(list) will
give a catalogue of functions intrinsic to lists. They are the analogue of c.conjugate()
in Section 3.3.4.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
30 A Short Python Tutorial
If start is zero, it may be omitted, e.g., u[ :-1] is a copy of u with the last element
omitted. The same applies at the other end, u[1: ] is a copy with the first element
omitted and u[:] is a copy of u. Here, we assume that the slice occurs on the right-hand
side of an assignation. The more general form of slicing is su = u[start:end:step].
Then su contains the elements u[start], u[start+step], u[start+2*step], . . . ,
as long as the index is less than start+end. Thus with the list u chosen as in the
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
3.5 Container Objects 31
Figure 3.2 Indices and slicing for a list u of length 8. The middle line shows the contents of u
and the two sets of indices by which the elements can be addressed. The top line shows the
contents of a slice of length 4 with conventional ordering. The bottom line shows another slice
with reversed ordering.
1 a=4
2 b=a
3 b=’foo’
4 a
5 b
6 u=[0,1,4,9,16]
7 v=u
8 v[2]=’foo’
9 v
10 u
The first five lines should be comprehensible: a is assigned to the object 4; so is b. Then
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
32 A Short Python Tutorial
b is assigned to the object ’foo’, and this does not change a. In line 6, u is assigned to
a list object and so is v in line 7. Because lists are mutable, we may change the second
element of the list object in line 8. Line 9 shows the effect. But u is pointing to the same
object (see Figure 3.1) and it too shows the change in line 10. While the logic is clear,
this may not be what was intended, for u was never changed explicitly.
It is important to remember the assertion made above: a slice of a list is always a
new object, even if the dimensions of the slice and the original list agree. Therefore,
compare lines 6–10 of the code snippet above with
1 u=[0,1,4,9,16]
2 v=u[ : ]
3 v[2]=’foo’
4 v
5 u
Now line 2 makes a slice object, which is a copy4 of the object defined in line 1.
Changes to the v-list do not alter the u-list and vice versa.
Lists are very versatile objects, and there exist many Python functions which can
generate them. We shall discuss list generation at many points in the rest of this book.
3.5.5 Tuples
The next container to be discussed is the tuple. Syntactically it differs from a list only by
using () instead of [] as delimiters, and indexing and slicing work as for lists. However,
there is a fundamental difference. We cannot change the values of its elements: a tuple is
immutable. At first sight, the tuple would appear to be entirely redundant. Why not use a
list instead? The rigidity of a tuple however has an advantage. We can use a tuple where
a scalar quantity is expected, and in many cases we can drop the brackets () when there
is no ambiguity, and indeed this is the commonest way of utilizing tuples. Consider the
snippet below, where we have written a tuple assignation in two different ways.
(a,b,c,d)=(4,5.0,1.5+2j,’a’)
a,b,c,d = 4,5.0,1.5+2j,’a’
The second line shows how we can make multiple scalar assignments with a single
assignation operator. This becomes extremely useful in the common case where we need
to swap two objects, or equivalently two identifiers, say a and L1. The conventional way
to do this is
temp=a
a=L1
L1=temp
4 For the sake of completeness, we should note that this is a shallow copy. If u contains an element which is
mutable, e.g., another list w, the corresponding element of v still accesses the original w. To guard against
this, we need a deep copy to obtain a distinct but exact copy of both u and its current contents. For further
details inspect the copy module. In other words, try import copy followed by the line copy?
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
3.5 Container Objects 33
This would work in any language, assuming temp, a and L1 all refer to the same type.
However,
a,L1 = L1,a
does the same job in Python, is clearer, more concise and works for arbitrary types.
Another use, perhaps the most important one for tuples, is the ability to pass a variable
number of arguments to a function, as discussed in Section 3.8.4. Finally, we note a
feature of the notation which often confuses the beginner. We sometimes need a tuple
with only one element, say foo. The construction (foo) strips the parentheses and
leaves just the element. The correct tuple construction is (foo,).
3.5.6 Strings
Although we have already seen strings in passing, we note that Python regards them as
immutable container objects for alphanumeric characters. There is no comma separator
between items. The delimiters can be either single quotes or double quotes, but not a
mixture. The unused delimiter can occur within the string, e.g.
L = [1,2,3,5,8,13]
ls = str(L)
ls
eval(ls) == L
Strings will turn out to be very useful for the input of data, and, most importantly,
producing formatted output from the print function (see Sections 3.8.6 and 3.8.7).
3.5.7 Dictionaries
As we have seen, a list object is an ordered collection of objects, A dictionary object
is an unordered collection. Instead of accessing the elements by virtue of their position,
we have to assign a keyword, an immutable object, usually a string, which identifies
the element. Thus a dictionary is a collection of pairs of objects, where the first item in
the pair is a key to the second. A key–object pair is written as key:object. We fetch
items via keys rather than position. The dictionary delimiters are the braces { }. Here is
a simple example that illustrates the basics.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
34 A Short Python Tutorial
1 empty={}
2 parms={’alpha’:1.3,’beta’:2.74}
3 #parms=dict(alpha=1.3,beta=2.74)
4 parms[’gamma’]=0.999
5 parms
Normally, Python executes statements in the order that they are written. The if state-
ment is the simplest way to modify this behaviour, and exists in every programming
language. In its simplest form the Python syntax is
if < Boolean expression >:
<block 1>
<block 2>
Here the expression must produce a True or False result. If True, then block 1 is
executed, followed by block 2, the rest of the program; if the expression is False, then
only block 2 is executed. Note that the if statement ends with a colon, :, indicating
that a block must follow. The absence of delimiters such as braces makes it much easier
to follow the logic, but the price required is careful attention to the indentation. Any
Python-aware editor should take care of this automatically.
Here is a very simple example that also illustrates strings in action.
x=0.47
if 0 < x < 1:
print "x lies between zero and one."
y=4
A simple generalization is
if < Boolean expression >:
<block 1>
else:
<block 2>
<block 3>
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
3.7 Loop Constructs 35
Here the truth of Boolean expression 1 leads to the execution of blocks 1 and 5. If
expression 1 is false and expression 2 is true, we get blocks 2 and 5. However, if expres-
sions 1 and 2 are false but 3 is true, only block 5 is executed, but if all three expressions
are false, both block 4 and block 5 are executed.
A situation which arises quite often is a construction with terse expressions, e.g.,
if x>=0:
y=f
else:
y=g
As in the C-family of languages, there is an abbreviated form. The snippet above can be
shortened in Python with no loss of clarity to
y=f if x>=0 else g
Computers are capable of repeating sequences of actions with great speed, and Python
has two loop constructs, for and while loops.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
36 A Short Python Tutorial
Here <iterable> is any container object. The <iterator> is any quantity which can
be used to access, term by term, the elements of the container object. If iterable is an
ordered container, e.g., a list a, then iterator could be an integer i within the range
of the list. The block above would include references to, e.g., a[i].
This sounds very abstract and needs to be elucidated. Many of the conventional C and
Fortran uses will be postponed to Chapter 4 because the core Python being described
here can offer only a very inefficient implementation of them. We start with a simple,
but unconventional example.5
c = 4
for c in "Python":
print c
c
Using c as the loop iterator here overwrites any previous use of c as an identifier. For
each character in the string iterable, the block is executed, which here merely prints its
value. When there are no more characters, the loop exits, and c refers to its last loop
value.
At first sight it looks as though <iterator> and <iterable> have to be single
objects, but, as will become normal, we can circumvent this requirement by using tuples.
For example, suppose Z is a list of tuples each of length 2. Then a loop with two iterators
can be constructed via
for (x,y) in Z:
<block>
and is perfectly permissible. For another generalization, consider the zip function in-
troduced in Section 4.4.1.
Before we can exhibit usage that is more traditional, we need to introduce the built-in
Python range function. Its general form is
range(start,end,step)
L=[1,4,9,16,25,36]
for it in range(len(L)):
L[it]+=1
L
5 If you are using a version of Python 3.0, this code will fail. See Section 3.8.7 to find out why, and the
trivial change needed to repair the snippet.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
3.7 Loop Constructs 37
Note that the loop is set up at the execution of the for statement. The block can
change the iterator, but not the loop. Try the simple example
for it in range(4):
it*=2
print it
it
It should be emphasized that, although the loop bodies in these examples are trivial,
this need not be so. Python offers two different ways of dynamically altering the flow
of control while the loop is executing. In a real-life situation, they could of course both
occur in the same loop.
If on any pass test2 produces a True value, the loop is exited and control passes to
<block5>. If <test2> always gives False, then the loop terminates in the usual way,
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
38 A Short Python Tutorial
and control passes first to <block4> and ultimately to <block5>. The author finds the
flow of control here to be counterintuitive, but the use of the else clause in this context
is both optional and rare. Here is a simple example.
y=107
for x in range(2,y):
if y%x == 0:
print y, " has a factor ", x
break
else:
print y, " is prime."
L1=[2,3,5,7,11,14]
L2=[] # Empty list
for i in range(len(L1)):
L2.append(L1[i]**2)
L2
However, Python can execute the loop in a single line via a list comprehension.
L1=[2,3,5,7,11,14]
L2=[x**2 for x in L1]
L2
Not only is this shorter, it is faster, especially for long lists, because there is no need to
construct the explicit for-loop.
List comprehensions are considerably more versatile than this. Suppose we want to
build L2, but only for the odd numbers in L1.
Suppose we have a list of points in the plane, where the coordinates of the points are
stored in tuples, and we need to form a list of their Euclidean distances from the origin.
import math
lpoints=[(1,0),(1,1),(4,3),(5,12)]
ldists=[math.sqrt(x*x+y*y) for (x,y) in lpoints]
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
3.8 Functions 39
Next suppose that we have a rectangular grid of points with the x-coordinates in one list
and the y-coordinates in the other. We can build the distance list with
l_x=[0,2,3,4]
l_y=[1,2]
l_dist=[math.sqrt(x*x+y*y) for x in l_x for y in l_y]
List comprehension is a Python feature which, despite its initial unfamiliarity, is well
worth mastering.
while <test>:
<block1>
<block2>
Here <test> is an expression which yields a Boolean object. If it is True, then <block1>
is executed. Otherwise, control passes to <block2>. Each time <block1> reaches its
end, <test> is re-evaluated, and the process repeated. Thus the construct
while True :
print "Type Control-C to stop this!"
3.8 Functions
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
40 A Short Python Tutorial
def <name>(<arglist>):
<body>
The def denotes the start of a function definition; <name> assigns an identifier or name
to address the object. The usual rules on identifier names apply, and of course the iden-
tifier can be changed later. The brackets () are mandatory. Between them, we may insert
zero, one or more variable names, separated by commas, which are called arguments.
The final colon is mandatory.
Next follows the body of the function, the statements to be executed. As we have seen
already, such blocks of code have to be indented. The conclusion of the function body is
indicated by a return to the same level of indentation as was used for the def statement.
In rare circumstances, we may need to define a function, while postponing the filling-
out of its body. In this preliminary phase, the body should be the single statement pass.
It is not mandatory, but conventional and highly recommended, to include a docstring, a
description of what the function does, between the definition and body. This is enclosed
in a pair of triple quotes, and can be in free format extending over one or more lines.
The inclusion of a docstring may seem to be an inessential frippery, but it is not. The
author of the len function included one which you accessed in Section 3.5.1 via len?.
Treat your readership (most likely you) with the same respect!
The body of the function definition introduces a new private namespace which is
destroyed when the execution of the body code terminates. When the function is in-
voked, this namespace is populated by the identifiers introduced as arguments in the
def statement, and will point to whatever the arguments pointed at when the function
was invoked. New identifiers introduced in the body also belong in this namespace.
Of course, the function is defined within a namespace which contains other external
identifiers. Those which have the same names as the function arguments or identifiers
already defined in the body do not exist in the private namespace, because they have
been replaced by those private arguments. The others are visible in the private names-
pace, but it is strongly recommended not to use them unless the user is absolutely sure
that they will point to the same object on every invocation of the function. In order to
ensure portability when defining functions, try to use only the identifiers contained in
the argument list and those which you have defined within, and intrinsic to, the private
namespace.
Usually we require the function to produce some object or associated variable, say y,
and this is done with a line return y. The function is exited after such a statement and
so the return statement will be the last executed, and hence usually the last statement
in the function body. In principle, y should be a scalar, but this is easily circumvented
by using a tuple. For example, to return three quantities, say u, v and w one should use
a tuple, e.g., return (u,v,w) or even return u,v,w. If, however¡ there is no return
statement Python inserts an invisible return None statement. Here None is a special
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
3.8 Functions 41
Python variable which refers to an empty or void object, and is the “value” returned
by the function. In this way, Python avoids the Fortran dichotomy of functions and
subroutines.
Here are some simple toy examples to illustrate the features. Try typing them into the
interpreter so that you verify the points made here and experiment further.
1 def add_one(x):
2 """ Takes x and returns x + 1. """
3 x = x+1
4 return x
5
6 x=23
7 add_one? # or help(add_one)
8 add_one(0.456)
9 x
In lines 1–4, we define the function add_one(x). There is just one argument x in-
troduced in line 1, and the object which it references is changed in line 3. In line 6, we
introduce an integer object referenced by x. The next line tests the docstring and the one
after that the function. The final line checks the value of x, which remains unchanged at
23 despite the fact that we implicitly assigned x to a float in line 8.
Next consider a faulty example:
1 def add_y(x):
2 """ Adds y to x and returns x+y. """
3 return x+y
4
5 add_y(0.456)
In line 5, private x is assigned to 0.456, and line 3 looks for a private y. None is found,
so the function looks for an identifier y in the enclosing namespace. None can be found
and so Python stops with an error. If, however, we introduce an instance of y before
calling the function
y=1
add_y(0.456)
then the function works as expected. However, this is non-portable behaviour. We can
use the function inside namespaces only where an instance of y has already been de-
fined. There are a few cases where this condition will be satisfied, but in general this
type of function should be avoided.
Better coding is shown in the following example which also shows how to return
multiple values via a tuple, and also that functions are objects.
1 def add_x_and_y(x, y):
2 """ Add x and y and return them and their sum. """
3 z=x+y
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
42 A Short Python Tutorial
4 return x,y,z
5
6 a, b, c = add_x_and_y(1,0.456)
7 a, b, c
The identifier z is private to the function, and is lost after the function has been left.
Because we assigned c to the object pointed to by z, the object itself is not lost when
the identifier z disappears. In the next two snippets we demonstrate that functions are
objects, and we can assign new identifiers to them. (It may be helpful to look again at
Figure 3.1 at this point.)
f = add_x_and_y
f(0.456, 1)
In all of these examples, the objects used as arguments have been immutable, and so
have not been changed by the function’s invocation. This is not quite true if the argument
is a mutable container, as is shown in the following example.
1 L = [0,1,2]
2 print id(L)
3 def add_with_side_effects(M):
4 """ Increment first element of list. """
5 M[0]+=1
6
7 add_with_side_effects(L)
8 print L
9 id(L)
The list L has not been changed. However, its contents can be, and have been, changed,
with no assignment operator (outside the function body). This is a side effect which is
harmless in this context, but can lead to subtle hard-to-identify errors in real-life code.
The cure is to make a private copy in line 5 below.
1 L = [0,1,2]
2 id(L)
3 def add_without_side_effects(M):
4 """ Increment first element of list. """
5 MC=M[ : ]
6 MC[0]+=1
7 return MC
8
9 L = add_without_side_effects(L)
10 L
11 id(L)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
3.8 Functions 43
In some situations, there will be an overhead in copying a long list, which may detract
from the speed of the code, and so there is a temptation to avoid such copying. However,
before using functions with side effects remember the adage “premature optimization is
the root of all evil”, and use them with care.
Every time foo1 is called, precisely three arguments have to be supplied. An example
might be y=foo1(3,2,1), and the obvious substitution by order takes place. Another
way of calling the function might be y=foo1(c=1,a=3,b=2), which allows a relax-
ation of the ordering. It is an error to supply any other number of arguments.
In calling such a function, we can give all arguments, or omit some, in which case the de-
fault value will be taken from the def statement. For example, invoking foo2(f=’b’)
will use the default values d=21.2 and e=4 so as to make up the required three argu-
ments. Since these are keyword arguments, the order does not matter.
It is possible to combine both of these forms of argument in the same function, pro-
vided all of the positional arguments come before the keyword ones. For example,
def foo3(a,b,c,d=21.2,e=4,f=’a’)
<block>
Now, in calling this function, we must give between three and six arguments, and the
first three refer to the positional arguments.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
44 A Short Python Tutorial
1 def average(*args):
2 """ Return mean of a non-empty tuple of numbers. """
3 print args
4 sum=0.0
5 for x in args:
6 sum+=x
7 return sum/len(args)
8
9 print average(1,2,3,4)
10 print average(1,2,3,4,5)
It is traditional, but not obligatory, to call the tuple args in the definition. The asterisk is,
however, mandatory. Line 3 is superfluous, and is simply to illustrate that the arguments
supplied really do get wrapped into a tuple. Note that, by forcing sum to be real in
line 4, we ensure that the division in line 7 works as we would expect, even though the
denominator is an integer.
4 show(1.3,2.7,3,’a’,4.2,alpha=0.99,gamma=5.67)
Beginners are not likely to want to use proactively all of these types of arguments.
However, they will see them occasionally in the docstrings of library functions, and so
it is useful to know what they are.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
3.8 Functions 45
On execution the first line issues the prompt question, and the keyboard input is encap-
sulated as a string. Thus the second line makes sense since we are printing the concate-
nation of two strings.
Now suppose that we want to key in, e.g., a list [1,2,3]. Using the snippet above,
name would refer to a string and so we would need the construct eval(name), see
Section 3.5.6, to recover the list object. Consider the alternative snippet
ilist = input(’Enter an explicit list’)
ilist
This works if we enter, say, [1,2,3]. If objname is the previously defined identifier of
a Python object, then that too could be entered.6 One or other of these snippets should
handle most keyboard-entry tasks.
In versions of Python 3.0, print is implemented as a function, and the code line
above becomes
print(<things to be printed>)
At the time of writing, parts of NumPy and its manifold extensions are available only
using the earlier versions. Since earlier versions of Python may eventually be depre-
cated, numerical users face a potential obsolescence dilemma, which is, however, re-
solved easily in one of two ways. The print function expects a variable number of
arguments, implicitly a tuple. If we use the second of the two code lines above with
earlier versions of Python, then the print command sees an explicit tuple because of the
() and prints <things to be printed> surrounded by brackets. If the extra brackets
prove annoying, then the alternative approach is to include the line
6 The naming of these two functions is confusing. input() returns the raw input, i.e., precisely the entered
keystrokes. However, raw_input() returns a string representation of them!
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
46 A Short Python Tutorial
at the top of the code, which will remove them when using the Python 3.0 print
function within Python < 3.0 code.
The print command expects a tuple as its argument. Thus, assuming it refers to an
integer and y to a float, we might write, dropping the tuple delimiters, simply
Here we have no control over the formatting of the two numbers, and this is a potential
source of problems, e.g., when producing a table, where uniformity is highly desirable.
A first attempt at a more sophisticated approach is
<string> % <tuple>
Here the string argument is printed with the %d term replaced by an integer and the %f
term replaced by a float, both chosen, in order, from the final tuple in the argument slot.
As written, the output of this version is identical to that of the previous line, but we can
improve significantly on that. The format codes summarized below are based on those
defined for the printf function in the C-family of languages.
We start by considering the format of the integer it, which we assume takes the value
41. If we replace %d in the code line with %5d, the output will be right justified in a field
of five characters, i.e., 41, where the character denotes a space. Similarly %-5d
will produce left justified output 41 . Also %05d will give 00041. If the number is
negative, then a minus sign counts as a character in the field. This can lead to untidi-
ness when printing both positive and negative integers. We can enforce the printing of
a leading plus or minus sign by choosing %+5d for right justification and %+-5d for left
justification. Of course, there is nothing special about the field width 5. It can be re-
placed by any suitable choice. Indeed, if the exact representation of the integer requires
more characters than have been specified, then Python overrides the format instruction
so as to preserve accuracy.
For the formatting of floats, there are three basic possibilities. Suppose y takes the
value 123.456789. If we replace %f by %.3f, the output will be 123.457, i.e., the value
is truncated and rounded to three decimal places. The format code %10.3f prints it
right justified in a field of width 10, i.e., 123.457, while %-10.3f does the same,
left justified. As with integer formatting, a plus sign immediately after the per cent sign
forces the printing of a leading plus or minus sign. Also %010.3f replaces the leading
spaces by zeros.
Clearly, the %f format will lose precision if y is very large or very small, and we con-
sider the case z=1234567.89. On the printed page, we might write z = 1.23456789 ×
106 and in Python z=1.23456789e6. Now consider the print format code %13.4e,
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
3.9 Introduction to Python Classes 47
which, applied to this z produces 1.2346e+06. There are precisely four integers af-
ter the decimal point, and the number is printed in a field thirteen characters wide. In
this representation, only ten characters are needed and the number is right justified with
three spaces to its left. Just as above, %-13.4e produces left justification and %+13.4e
produces a leading sign, and only two spaces. (%+-13.4e does the same with left jus-
tification.) If the width is less than the minimum of ten required here, then Python will
increase it to ten. Finally, replacing ’e’ by ’E’ in these codes ensures an upper case
character, e.g., %+-13.4E produces +1.2346E+06 .
Sometimes we need to print a float whose absolute value can vary widely, but we
wish to display a particular number of significant digits. With z as above, %.4g will
produce 1.235e+06, i.e., correct to four significant digits. Note that %g defaults to %.6g.
Python will use whichever of the ’e’ and ’f’ formats is the shorter. Also %.4G chooses
between the ’E’ and ’f’ formats.
For completeness, we note that string variables are also catered for, e.g., %20s will
print a string in a field of at least 20 characters, with left padding by spaces if needed.
f = lambda x, y : x, y, x+y
or just
lambda x, y : x, y, x+y
See Sections 4.1.5 and 8.5.3 for more realistic cases of anonymous functions.
Classes are extremely versatile structures in Python. Because of this, the documentation
is both long and complicated. Examples in the literature tend to be drawn from non-
scientific data-processing and are often either too simple or unduly complicated. The
basic idea is that you may have a fixed data structure or object which occurs frequently,
together with operations directly associated with it. The Python class encapsulates both
the object and its operations. Our introductory presentation by way of a scientific exam-
ple is concise but contains many of the features most commonly used by scientists. In
this pedagogic context, we shall use integer arithmetic.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
48 A Short Python Tutorial
Our example is that of fractions, which can be thought of as arbitrary precision rep-
resentations of real numbers. We have in mind to implement a Frac as a pair of integers
num and den, where the latter is non-zero. We have to be careful, however, for 3/7 and
24/56 are usually regarded as the same number. We looked at this particular problem
at the end of Chapter 2, and we will need access to the file gcd.py created there. The
following snippet, which shows a skeleton Frac class, relies on it
1 # File frac.py
2
3 import gcd
4
5 class Frac:
6 """ Fractional class. A Frac is a pair of integers num, den
7 (with den!=0) whose GCD is 1.
8 """
9
10 def __init__(self,n,d):
11 """ Construct a Frac from integers n and d.
12 Needs error message if d = 0!
13 """
14 hcf=gcd.gcd(n, d)
15 self.num, self.den = n/hcf, d/hcf
16
17 def __str__(self):
18 """ Generate a string representation of a Frac. """
19 return "%d/%d" % (self.num,self.den)
20
21 def __mul__(self,another):
22 """ Multiply two Fracs to produce a Frac. """
23 return Frac(self.num*another.num, self.den*another.den)
24
25 def __add__(self,another):
26 """ Add two Fracs to produce a Frac. """
27 return Frac(self.num*another.den+self.den*another.num,
28 self.den*another.den)
29
30 def to_real(self):
31 """ Return floating point value of Frac. """
32 return float(self.num)/float(self.den)
33
34 if __name__=="__main__":
35 a=Frac(3,7)
36 b=Frac(24,56)
37 print "a.num= ",a.num, ", b.den= ",b.den
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
3.9 Introduction to Python Classes 49
38 print a
39 print b
40 print "floating point value of a is ", a.to_real()
41 print "product= ",a*b,", sum= ",a+b
The first novelty here is the class statement in line 5. Note the terminating colon.
The actual class is defined through indentation and here extends from line 5 to line 32.
Lines 6–8 define the class docstring for online documentation. Within the class, we
have defined five class functions, which are indented (because they are in the class) and
which have further indented bodies (as usual).
The first one, lines 10–15, would in other languages be called a “constructor” func-
tion. Its purpose is to turn pairs of integers into Frac objects. The mandatory name
__init__ is strange, but, as we shall see, it is never used outside the class defini-
tion. The first argument is usually called self, and it too never appears outside the
class definition. This all looks very unfamiliar, so look next at line 35 of the test suite,
a=Frac(3,7). This is asking for a Frac object for the integer pair 3,7 to be referenced
by the identifier a. Implicitly, this line invokes the __init__ function with self re-
placed by a, and n and d replaced by 3 and 7. It then computes hcf, the GCD of 3
and 7 (here 1), and in line 15 it computes a.num as n/hcf and a.den as d/hcf. These
are accessible in the usual way, and in line 37 the first of them gets printed. The same
applies to the assignment in line 36, where __init__ gets called with self replaced
by b.
Almost every class would benefit from having lines like 38–39, where class objects
are “printed”. This is the purpose of the class string function __str__, which is defined
in lines 17–19. When line 38 is executed, __str__ is invoked with self replaced by a.
Then line 19 returns the string "3/7", and this is what is printed.
While these first two class functions are more or less essential, we may define many
or few functions to carry out class operations. Let us concentrate on multiplication and
addition, according to the standard rules
n1 n2 n1 n2 n1 n2 n1 d2 + d1 n2
∗ = , + = .
d1 d2 d1 d2 d1 d2 d1 d2
Multiplication of two Frac objects requires the definition of the class function __mul__
in lines 21–23. When we invoke a*b in line 41, this function is invoked with self re-
placed by the left operand, here a, and another replaced by the right operand, here b.
Note how line 23 computes the numerator and denominator of the product, and then
calls __init__ to create a new Frac object, so that c=a*b would create a new Frac
with identifier c. In line 41 though, the new, as yet unnamed, Frac is passed immedi-
ately to __str__. As the code snippet shows, addition is handled in exactly the same
way.
Note how, in line 37, we were able to access the constituent parts of the class instance
a. Note also that in line 40 we used the function to_real() associated with the same
class instance. These are class attributes and we shall make increasing use of this “dot
mechanism”, which permeates Python, see the next section.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
50 A Short Python Tutorial
For the sake of brevity, we have left the class incomplete. It is a valuable exercise for
the beginner to enhance the code gradually in ways which are more advanced.
1. Create division and subtraction class functions called __div__ and __sub__, and
test them.
2. It looks a little odd if the denominator turns out to be 1 to print, e.g., 7/1. Amend
the __str__ function so that if self.den is 1, then the string created is precisely
self.num, and test that the new version works.
3. The __init__ code will certainly fail if argument d is zero. Think of warning the
user.
Near the end of Section 3.2, we pointed out the relationship between identifiers and
objects, and we now return to this topic. In our pedagogic example of a Python class
Frac in Section 3.9, we noted that an instance of a Frac, e.g., a=Frac(3,7), produces
an identifier a referring to an object of class Frac. Now a Frac object contains data,
here a pair of integers, together with a set of functions to operate on them. We access
the data relevant to the instance via the “dot mechanism”, e.g., a.num. Similarly, we
access the associated functions via, e.g., a.to_real().
So far, this is merely gathering previously stated facts. However, Python is packed
with objects, some very complicated, and this “dot mechanism” is used universally to
access the objects’ components. We have already seen enough of Python to point out
some examples.
Our first example is that of complex numbers described in Section 3.3.4. Suppose
we have set c=1.5-0.4j or equivalently c=complex(1.5,-0.4). We should regard
complex numbers as being supplied by class Complex, although to guarantee speed they
are hard-coded into the system. Now, just as with the Frac class, we access the data via
c.real and c.imag, while c.conjugate() produces the complex conjugate number
1.5+0.4j. These are further examples of instances and attributes. We say that Python
is object oriented. Compare this approach with that of a function-oriented language such
as Fortran77, where we would have used C=CMPLX(1.5,-0.40), REAL(C), AIMAG(C)
and CONJG(C). At this simple level, there is no reason to prefer one approach over the
other.
Our next example refers to the modules described in Section 3.4. Like most other
features in Python, a module is an object. Thus import math as re includes the math
module and gives it the identifier re. We can access data as, e.g., re.pi and functions
by, e.g., re.gamma(2).
Once you master the concept of the “dot mechanism”, understanding Python should
become a lot clearer. See, e.g., the discussion of container objects in Section 3.5. All of
the more sophisticated packages, e.g., NumPy, Matplotlib, Mayavi, SymPy and Pandas
rely on it. It is at this level that the object-oriented approach pays dividends in offering
a uniform environment which is not available in early versions of C or Fortran.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
3.11 Prime Numbers: A Worked Example 51
We finish this chapter on “pure” Python by looking at a real problem. The internet has
revolutionized communications and has emphasized the need for security of transmis-
sion of data. Much of this security is based on the fact that it is exceedingly difficult to
decide whether a given integer n which is large, say n > 10100 , can be written as the
product of prime numbers. We look here at a much more fundamental problem, building
a list of prime numbers.
Table 3.1 The Sieve of Eratosthenes for primes 18. We start by writing down in a row the
integers from 2 to 18. Starting with the leftmost integer, we delete all multiples of it in line 2.
Then we move to the closest remaining integer, here 3, and delete all multiples of 3 in line 3.
We continue this process. Clearly, the numbers left are not products of smaller integers; they
are the primes.
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
2 3 5 7 9 11 13 15 17
2 3 5 7 11 13 17
2 3 5 7 11 13 17
1 def sieve_v1(n):
2 """
3 Use Sieve of Eratosthenes to compute list of primes <= n.
4 Version 1
5 """
6 primes=range(2,n+1)
7 for p in primes:
8 if p*p>n:
9 break
10 product=2*p
11 while product<=n:
12 if product in primes:
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
52 A Short Python Tutorial
13 primes.remove(product)
14 product+=p
15 return len(primes),primes
The first five lines are conventional. In line 6, we introduce the top line of Table 3.1
coded as a Python list called primes, which is to be sieved. Next we introduce a for
loop, which runs for lines 7–14. Each choice for the loop variable p corresponds to a
√
new row in the table. As we remarked above, we need not consider p > n, and this
is tested in lines 8 and 9. The break command transfers control to the line after the
end of the loop, i.e., line 15. (This can be seen from the indentation.) Now look at the
while loop which runs for lines 11–14. Because of the earlier break statement, it is
guaranteed that the while loop is entered at least once. Then if product is still in the
list, line 13 deletes it. In Section 3.5.1, we saw that list.append(item) appends item
to the list. Here list.remove(item) deletes the first occurrence of item in list. It
is an error if item is not in list, and this is why its presence is guaranteed by line 12.
(Try help(list) or list? to see the methods available for lists.) Having potentially
removed 2*p, we next construct 3*p in line 14 and repeat the process. Once all relevant
multiples of p have been removed by iteration of the while loop, we return to line 7 and
set p to be the next prime in the list. Finally, we return the list of primes and its length.
The latter is an important function in number theory, where it is usually denoted π(n).
You are recommended to construct a file called sieves.py and type or paste this
code into it. Then in IPython type
so as to check that things are working. You can even check the time taken with a com-
mand7
timeit sieve_v1(1000)
Inspection of Table 3.2 shows that, while the performance of this simple straight-
forward function is satisfactory for small n, the time taken grows unacceptably large
for even moderately large values. It is a very useful exercise to see how easily we can
improve its performance.
We start by considering the algorithm, which involves two loops. We can hardly avoid
iterating over the actual primes, but for each prime p we then loop, removing composite
numbers n × p, where n = 2, 3, 4, . . ., from the loop. We can make two very simple
improvements here. Note that, assuming p > 2, any composite number less than p2
will have been removed already from the sieve, and so we may start by removing the
composite p2 . Further, the composite p2 + n × p is even if n is odd, and so was removed
from the sieve on the first pass. Therefore, we can improve the algorithm for each prime
7 The IPython %run -t introduced in Section 2.5 works only on complete scripts. However, the magic
%timeit command times individual lines.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
3.11 Prime Numbers: A Worked Example 53
Table 3.2 The number of primes π(n) n, and the approximate time taken on the author’s
laptop to compute them using the Python functions given here. The relative timings (not the
absolute ones) are of interest here.
p > 2, by removing just p2 , p2 + 2p, p2 + 4p, . . . . This is not the best we could do, e.g.,
63 is sieved twice, but it will suffice.8
Next we consider the implementation. Here we have been rather wasteful in that five
loops are involved. The for and while loops are explicit. The if statement in line 12
involves iterating through the primes list. This is repeated in line 13, and after product
has been identified and discarded, all of the remaining list elements have to be shuffled
down one position. Consider the following non-obvious re-implementation.
1 def sieve_v2(n):
2 """
3 Sieve of Eratosthenes to compute list of primes <= n.
4 Version 2.
5 """
6 sieve = [True]*(n+1)
7 for i in xrange(3,n+1,2):
8 if i*i > n:
9 break
10 if sieve[i]:
11 sieve[i*i: :2*i]=[False]*((n - i*i) // (2*i) + 1)
12 answer = [2] + [i for i in xrange(3,n+1,2) if sieve[i]]
13 return len(answer), answer
Here the initial sieve is implemented as a list of Booleans, all initialized to True in
line 6. This will be a block of code, fast to set up, and occupying less memory than a
list of the same length of integers. The outer loop is a for loop running from lines 7
to 11. In line 7, the function xrange is new. This behaves just like range, but rather
than creating an entire list in memory, it generates the members on demand. For large
lists, this is faster and less memory-hungry. Here the loop covers the odd numbers in the
√
closed interval [3, n]. Lines 8 and 9 stop the outer loop once i > n, just as in the earlier
version. Now consider lines 10 and 11. Initially, i is 3, and sieve[i] is True. We need
8 Sieves which are more elaborate exist, e.g., those of Sundaram and of Atkin.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
54 A Short Python Tutorial
to set the sieve elements for i2 , i2 + 2i, . . . to False, the equivalent of discarding them in
this implementation. Rather than use a loop, we carry out the operation as a single slice
in line 11. (The integer factor on the right gives the dimension of the slice.) At the end of
the for loop, the list sieve has had set to False the items with indices corresponding
to all odd composite integers. Finally, in line 12 we build the list of primes. We start
with a list containing just 2. We use a list comprehension to construct a list of all odd
unsieved numbers, and then we concatenate both lists, before returning the result.
Although at first reading it may take some time to understand this version, nothing
new (other than xrange) has been used, the code is 25% shorter and runs considerably
faster, as shown in the final column in Table 3.2. Indeed, it delivers the millions of
primes less than 108 in a few seconds. The extension to 109 would take a few minutes
if tens of gigabytes of memory were available. Clearly, for very large primes, sieve
methods are not the most efficient.
Notice too that within the lists in this second version, the items all have the same
type. Although Python does not impose this restriction, it turns out to be worthwhile
to introduce a new object, lists of homogeneous type, and this leads naturally to the
NumPy, the topic of the next chapter.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.005
4 NumPy
c=[]
for i in range(len(a)): c.append(a[i]*b[i])
However, if the lists contain thousands or millions of items, this will be extremely slow.
Had we been using a compiled language, e.g., C, and ignoring all variable declarations
etc., we might have written
Although superficially complicated, this code will execute extremely quickly. However,
with ndarrays the code would be
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
56 NumPy
c=a*b
and would use precompiled code to achieve (very nearly) the same speed. To be fair,
modern compiled languages, e.g., C++ and Fortran90, can achieve the same simplicity
of expression for such a simple example. NumPy’s library of “vectorized” functions and
operations is, however, at least as rich as those available in compiled languages, and
becomes considerably richer once SciPy, see Section 4.9.1, is taken into account.
This chapter is an introduction to NumPy. While the core is pretty stable, extensions
near the edges are an ongoing process. The definitive documentation is a recent “user
guide”, Numpy Community (2017b) at 130 pages and the “reference manual”, NumPy
Community (2017a) at 1534 pages. Earlier, more discursive accounts, each including a
wealth of examples, can be found in Langtangen (2009) and Langtangen (2014).
Before we start, we must import the NumPy module. The preferred approach is to
preface the code with
import numpy as np
and to drop the np. prefix. While this is fine for small-scale experiments, experience
shows that it tends to lead to namespace difficulties for real-life problems. We shall
assume that the first import statement above has always been used.
Once NumPy has been imported, we need to think about on-line documentation, for
this is a very large module. It is very straightforward to see how large NumPy is if
you are following my recommendation to use the IPython interpreter, which utilizes
tab completion, i.e., if we type part of a command and press the tab key, the range of
possible completions is shown, unless there is precisely one, in which case it will be
completed. Trying
np.<TAB>
shows that over 500 possibilities are available. How do we make sense of this plethora?
In my view, the best place to start is the np.lookfor function; try
np.lookfor? # or help(np.lookfor)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
4.1 One-Dimensional Arrays 57
Vectors or one-dimensional arrays are the basic building blocks for numerical computa-
tion. We look first at how to construct them, and then at how to use them. It makes sense
here to distinguish two types of constructors, build a vector from scratch, or construct a
vector to “look like” another object.
x is an array of length num. If num is not specified, it defaults to the value 50. The
first element is x[0]=start. If endpoint is True, the default, then the final value is
x[-1]=stop, and the interval spacing is step=(stop-start)/(num-1). However, if
we choose endpoint to be False, then step=(stop-start)/num, so the final value
is x[-1]=stop - step. Thus the parameter endpoint controls whether the interval
is closed [start, stop] or half open [start, stop). If retstep is True, then the function
returns a tuple consisting of the array and step. The following code snippets illustrate
the basic use of np.linspace.
import numpy as np
xc,dx=np.linspace(0,1,11,retstep=True)
xc,dx
xo=np.linspace(0,1,10,endpoint=False)
xo
The function np.logspace, is similar, but there the numbers are equally spaced on a
logarithmic scale. See its docstring for the details and examples of use.
Somewhat closer to the range function of Python is the function, np.arange, which
returns an array rather than a list. The calling sequence is
x=np.arange(start=0,stop,step=1,dtype=None)
which generates the open interval [start, stop) with intervals of step. Python will try to
deduce the type of the array, e.g., int, float or complex, from the input arguments, but
this choice can be overridden by specifying the type in the last argument. Two examples
are
import numpy as np
yo=np.arange(1,10)
yoc=np.arange(1,10,dtype=complex)
yo, yoc
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
58 NumPy
constructs an array of length num filled with zeros. The function np.ones does the same
but packs the array with ones, and np.empty constructs an array of the same length but
leaves the values of the contents unspecified.
Last but not least, we need to introduce the function np.array. Its simplest and most
used form is
ca=np.array(c,dtype=None,copy=True)
Here c is any container object which can be indexed, e.g., a list, a tuple or another array.
The function will try to guess an appropriate type, but this can be overwritten by using
the dtype parameter. Possible choices include bool, int, float, complex and even
user-defined objects, see Section 3.9. Here are two examples, one from a list and one
converting an array of floats to a complex array.
la=np.array([1,2,3.0])
x = np.linspace(0,1,11)
cx=np.array(x, dtype=complex)
la, x, cx
will generate a new array with identifier y whose components are the sines of the corre-
sponding components of x.
Otherwise, a very useful constructor is np.empty_like
xe=np.empty_like(x)
sets up an empty array (i.e., the contents have unspecified values) of the same size and
type as x. Almost as useful are np.zeros_like and np.ones_like, which behave
very much like np.empty_like.
Surprisingly often, we need vectors that span an interval but with different spacings
on different subintervals. To take a concrete example, suppose we require xs to span
[0, 1] with a spacing of 0.1, except on [0.5, 0.6], where we require a spacing of 0.01.
We achieve this by constructing three subintervals, two half-closed and one closed, and
then joining them together. It is a useful exercise to consider carefully the following
snippet.
1 xl=np.linspace(0,0.5,5,endpoint=False)
2 xm=np.linspace(0.5,0.6,10,endpoint=False)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
4.1 One-Dimensional Arrays 59
3 xr=np.linspace(0.6,1.0,5)
4 xs=np.hstack((xl,xm,xr))
5 xs
Note that np.hstack accepts only one argument, and so we must use a tuple in line 4.
We end this section by remarking that a vector is a one-dimensional instance of an
ndarray. Any ndarray is a mutable container object. We studied some of the properties
of such objects in the discussion of lists in Section 3.5. The remarks we made there,
especially on slicing and copies of lists, apply equally well to vectors, and indeed to
more general ndarrays.
Let us look in detail at the first item in line 3. The sum is an array of the same size
as its operands. The ith component is the sum of the i-components of a and c. In this
sense, the + operator is said to act component-wise. All of the arithmetical operations in
this code snippet are acting component-wise. Incidentally, there is an efficiency issue,
relevant for large arrays, which should, perhaps, be highlighted here. Suppose in line 3
of the snippet above we had set a=a+c. Python would have created a temporary array
to hold the sum required on the right-hand side, filled it, then attached the identifier a
to it, and finally deleted the original a-array. It is faster to use a+=c, which avoids the
creation of a temporary array. Similar constructions are available for the other arithmetic
operators acting on vectors. There is, however, a pitfall for the unwary. In Python, if a
scalar a has type int and a second scalar b has type float, then the operation a+=b
widens the type of a to float. In fact, the precise opposite holds for NumPy arrays! The
reader is encouraged to try
1 a=np.ones(4,dtype=int)
2 b=np.linspace(0,1,4)
3 a+=b, a
to see that the type of a is unchanged. The numbers in the new a have been narrowed
back to int by truncation towards zero.
In general, arithmetical operations between vectors of different sizes which produce
another vector cannot be defined unambiguously, and so cause an error. However, such
operations between an array and a scalar can be given an unambiguous meaning.
1 import numpy as np
2 a=np.linspace(0,1,5)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
60 NumPy
Here the last entry deserves comment. The sum (or difference) of an array, here a, and a
scalar, here 2, is not defined. However, NumPy effectively1 chooses to widen or broad-
cast 2 to 2*ones_like(a) and performs the addition component-wise. Broadcasting
is discussed in more detail in Section 4.2.1.
We now have enough information to consider a simple but non-trivial example, the
smoothing of data by three-point averaging. Suppose f refers to a Python vector of data.
We might smooth the data at interior points as follows:
This works well for small arrays but becomes very slow for larger vectors. Consider
instead
f_av=f.copy() # as above
f_av[1:-1]=(f[ :-2]+f[1:-1]+f[2: ])/3.0
This vectorized code will execute much faster for large arrays since the implied loop
will be executed using precompiled C code. One way to get the slicings correct is to
note that, in each slice [a:b], the difference a-b is the same, i.e., 2.2 Finally, we need
to point out to beginners that there is an extremely important difference between arrays
and lists. If l is a Python list, then l[ : ] is always a (shallow) copy, but for NumPy
arrays, slicings always reference the original array. That is why we had to enforce an
explicit (deep) copy in the code snippets above.
4.1.4 Ufuncs
The class of universal functions or ufuncs adds considerably to the usefulness of NumPy.
A ufunc is a function which when applied to a scalar generates a scalar, but when applied
to an array produces an array of the same size, by operating component-wise. Some of
the ufuncs which are most useful for scientists are shown in Table 4.1. Many of these
will be familiar, but the docstrings are readily available, e.g.,
np.fix? # or help(np.fix)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
4.1 One-Dimensional Arrays 61
Table 4.1 Some commonly used ufuncs which can be applied to vectors. Those in the last
column require two arguments. For documentation, try, e.g., np.sign? or help(np.sign).
The module NumPy behaves differently, and the type of the argument is critical. Note
that np.sqrt(-1+0j) takes a complex square root and returns 1j, but np.sqrt(-1)
produces a warning and returns np.nan or not a number, a float of indeterminate value.
Further arithmetic may be performed on it, but the result will always be np.nan. Similar
remarks apply to cos−1 2.
In NumPy, direct division by zero 1.0/0.0 produces an error and the execution halts.
However, if it occurs indirectly, e.g., within a loop, this is not the case. Consider
x = np.linspace(-2, 2, 5)
x, 1.0/x
Using the techniques of Chapter 3, we might try to apply this definition via the code
import numpy as np
def h(x):
""" return 1 if 0<=x<=1 else 0. """
if x < 0.0:
return 0.0
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
62 NumPy
v=np.linspace(-2, 2, 401)
hv=h(v)
However, this fails, and the reason is very simple. If x has more than one element, then
x<0.0 is ambiguous. The next subsection shows how to resolve this problem.
import numpy as np
x=np.linspace(-2,2,9)
y=x<0
x, y
The primary result is that y is a vector of length 9 of type bool, of which precisely the
first four components are True. This shows that the surmise above was indeed correct.
The next code snippet demonstrates an extremely useful feature of NumPy.
z=x.copy()
z[y]=-z[y]
z
We may treat logical arrays such as y as slice definitions on one or both sides of an as-
signment. First we make a copy z of x. The right-hand side of the next line first selects
those components of z which are negative, i.e., those for which the corresponding com-
ponent of y is True, then multiplies them by −1. The assignment then inserts precisely
the modified components back into z. Thus we have computed z=|x| using implicit,
C-style, loops. It should be clear that the intermediate logical array y is redundant. We
could compute z=|x| more quickly and clearly by
z=x.copy()
z[z<0]=-z[z<0]
The reason for the copy is to leave the original x unchanged. See the warning at the end
of Section 4.1.3.
Acting on scalars, we can combine logical operators, e.g., x>0 and x<1, by chaining
them, e.g., 0<x<1. Acting on arrays, we have to carry out the comparisons individually.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
4.1 One-Dimensional Arrays 63
As an example, we might compute the hat function h(x), defined in the previous sub-
section, for the current vector x via
1 h=np.ones_like(x)
2 h[x<0]=0.0
3 h[x>1]=0.0
4 h
Suppose we want to construct a more complicated function, e.g., the example k(x)
below. The NumPy module offers a function select of considerable generality to en-
capsulate a number, say M, of possible choices. Because a formal description is rather
complicated, the reader is advised to look ahead to the example and corresponding code
snippet while reading it. We assume that x is a one-dimensional array of length n and
label its elements xi (0 i < n). We assume the choices form a list C with members C J
(0 J < M). For example, C0 might be x 2. Next we might construct a list B of an-
swers of length M. Each member BJ is an array of Booleans of length n whose elements
are defined by BJi = C J (xi ). Finally, we need to supply a list of outcomes R of length
M. Each member R J is an array of length n whose elements are defined according to
⎧
⎪
⎪
⎨desired outcome if BJi = True,
R Ji = ⎪
⎪
⎩arbitrary if BJi = False.
Then the select function produces a one-dimensional array k of length n, and operates
as follows. There is an implicit outer loop over i where 0 i < n, and an implicit
inner loop over J, where 0 J < M. For each fixed i, we search through the array BJi
skipping all the False values until we reach the first True one. We then set ki equal to
the corresponding R Ji , break out of the J-loop and move to the next i. There is a potential
complication. If BJi returns False for fixed i and all J, then the above procedure would
produce an arbitrary value for ki . To guard against this possibility, it might be prudent
to supply a default scalar value for ki to be used in such cases.
The formal syntax is, assuming that x, C and R are already defined, and m = 3
result=np.select([C0, C1, C2], [R0, R1, R2], default=0)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
64 NumPy
or more succinctly
x=np.linspace(-1,3,9)
k=np.select([ x>=2, x>=1, x>=0, x<0 ],
[ 4.0, x**2, x**3, -x ])
Note that the order of the choices (and their outcomes) needs some care. Can you find a
different ordering?
In a certain sense, the select function is wasteful, for all possible outcomes need to
be evaluated in advance. If this is a performance issue, then NumPy offers an alternative
approach called piecewise, involving functions. Consider the example
⎧ 2x
⎪
⎪
⎪ e if x < 0,
⎪
⎪
⎨
m(x) = ⎪
⎪1 if 0 x < 1,
⎪
⎪
⎪
⎩e1−x if 1 x.
1 def m1(x):
2 return np.exp(2*x)
3 def m2(x):
4 return 1.0
5 def m3(x):
6 return np.exp(1.0-x)
7
8 x=np.linspace(-10,10,21)
9 conditions=[ x>=0, x>=1, x<0 ]
10 functions=[ m2, m3, m1 ]
11 m=np.piecewise(x, conditions, functions)
12 m
Now the functions will be called only where they are needed. Note that the length of
the functions list should be either the same as that of the conditions list, or one larger.
In this case, the final function is the default choice. There are other options, see the
np.piecewise docstring for details.
It is perhaps unlikely that the functions m1, m2 and m3 in the snippet above will be
used elsewhere. However, the syntax of np.piecewise requires functions to be present,
and so this is a situation where the anonymous functions of Section 3.8.8 can be useful.
Using them, a more self-contained definition might be
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
4.2 Two-Dimensional Arrays 65
We turn next to two-dimensional arrays. For this, and the more general case of n-
dimensional arrays, the official documentation is far from perfect. Fortunately, once one
has mastered the basic definitions, it is easy to see that the definitions are consistent with
those for vectors, and that much of what we have already learned about one-dimensional
arrays or vectors carries through.
A general NumPy array carries three important attributes: ndim, the number of di-
mensions or axes; shape, a tuple of dimension ndim, which gives the extent or length
along each axis; and dtype, which gives the type of each element. Let see us them first
in a familiar context.
v=np.linspace(0,1.0,11)
v.ndim, v.shape, v.dtype
Here the shape is (11,), which is a tuple with one element and is almost, but not quite,
synonymous with the number 11.
A conceptually simple method of generating two-dimensional arrays explicitly is
from a list of lists, e.g.,
x=np.array([[0,1,2,3],[10,11,12,13],[20,21,22,23]])
x
Note first the display produced by the last line of the code snippet. The trailing axis,
corresponding to the last element of the shape, is displayed horizontally, then the next
axis, corresponding to the penultimate element of the shape, is displayed vertically. (For
large arrays, the default option is that only the corners are printed.) The next snippet
shows how we can access individual rows and columns.
x[2], x[ : ,1], x[2][1], x[2,1]
The good news is that the Python slicing conventions still apply. The last two items
show ways to access an individual element of x. In the first we create an intermediate
temporary vector from the last row, and then access an element of that vector. It is, how-
ever, more efficient, especially for large arrays, to access the required element directly
as in the last entry.
4.2.1 Broadcasting
Suppose that y is an array with precisely the same shape as x. Then x+y, x-y, x*y
and x/y are arrays of the same shape, where the operations are carried out component-
wise, i.e., component by component. In certain circumstances, these operations are well-
defined even when the shapes of x and y differ, and this is called broadcasting. We
generalize to a situation where we have a number of arrays, not necessarily of the same
shape, to which we are applying arithmetical operations. At first sight broadcasting
seems somewhat strange, but it is easier to grasp if we remember two rules:
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
66 NumPy
• The first rule of broadcasting is that, if the arrays do not have the same number of
dimensions, then a “1” will be repeatedly prepended to the shapes of the smaller
arrays until all the arrays have the same number of axes.
• The second rule of broadcasting ensures that arrays with a size of 1 along a particular
dimension or axis act as if they had the size of the array with the largest size along
that dimension. The value of the array element is assumed to be the same along
that dimension for the “broadcasted” array.
As a simple example, consider the array v with shape (11,) introduced on the pre-
vious page. What does 2*v mean? Well, 2 has shape (0,) and so by the first rule we
augment the shape of “2” to (1,). Next we use the second rule to increase the shape of
“2” to (11,), with identical components. Finally, we perform component-wise multi-
plication to double each element of v. The same holds for other arithmetic operations.3
However, if w is a vector with shape (5,), the broadcasting rules do not allow for the
construction of v*w.
To see array arithmetic with broadcasting in action, consider
1 x=np.array([[0,1,2,3],[10,11,12,13],[20,21,22,23]])
2 r=np.array([2,3,4,5])
3 c=np.array([[5],[6],[7]])
4 print "2*x = \n", 2*x
5 print "r*x = \n", r*x
6 print "x*r = \n", x*r
7 print "c*x = \n", c*x
Note that, since x*r=r*x, x*r is not the same as matrix multiplication and neither
is x*x. For this, try np.dot(x,r) or np.dot(x,x). A brief introduction to matrix
arithmetic will be given in Section 4.8.
3 We have already seen this in Section 4.1.3. The discussion there is fully consistent with the broadcasting
rules here.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
4.2 Two-Dimensional Arrays 67
By constructing either or both of the vectors xv and yv using np.arange, we can deal
with the half-open possibilities. Notice from the shape of xa or ya that np.meshgrid
uses image form.
The np.mgrid and np.ogrid operators use rather different syntax, cobbled together
from slicing notation, and the representation of complex numbers. We illustrate this first
in one dimension. Try out the two lines
np.mgrid[-1:1:9j]
np.mgrid[-1:1:0.25]
Note that the first line, with pure imaginary spacing, mimics the effect of the one-
dimensional np.linspace while the second emulates np.arange. Although unfamil-
iar, this notation is succinct and generalizes to two or more dimensions, where it uses
matrix form.
1 [xm,ym]=np.mgrid[-1:1:5j, 0:1:3j]
2 print "xm = \n", xm
3 print "ym = \n", ym
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
68 NumPy
This is shorter than the first code snippet of this section, but achieves the same result
up to a linear transformation. The adaptation to half-closed intervals is handled by the
snippet before this one.
For large arrays, especially with more dimensions, much of the data in xm and ym
may be redundant. This deficiency is addressed by the np.ogrid variant which also
uses matrix form.
1 [xo,yo]=np.ogrid[-1:1:5j, 0:1:3j]
2 print "xo = \n", xo
3 print "yo = \n", yo
4 print "xo.shape = ", xo.shape, " yo.shape = ", yo.shape
5 print "xo*yo = \n", xo*yo
You should verify that xo and yo have shapes chosen so that the broadcasting rules
apply, and that xm*ym (from the last snippet) and xo*yo are identical.
In the previous section, we introduced the three functions np.zeros, np.ones and
np.empty as vector constructors. They work equally well as constructors for more gen-
eral arrays. All that is necessary is to replace the first argument, which was the length
of the vector, by a tuple defining the shape of the array, e.g.,
x=np.zeros((4,3),dtype=float)
defines a 4 × 3 array of floats, in matrix form, initialized to zeros. Of course, a tuple with
one element, e.g., (9,), can be replaced by an integer, e.g., 9 in this context.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
4.3 Higher-Dimensional Arrays 69
Ufuncs, which take a single argument, e.g., np.sin(x), work exactly as expected
for arrays. Those which take more arguments, e.g., np.power(x,y), also work exactly
as expected if the arguments have the same shape, and are subject to the broadcasting
rules otherwise.
Very little needs to be said about slicing. Although various shortcuts do exist, it is
safest and clearest to exhibit both dimensions, and we can slice one or both dimensions
in the obvious way, e.g.
1 vg=np.reshape(np.linspace(0,7,15),(5,3))
2 vg
3 vg[1:-1,1: ]=9
4 vg
The good news here is that almost everything that has been said about two-dimensional
arrays carries over into higher dimensions. The single exception is the np.meshgrid
function, which is restricted to two dimensions. Here is a simple but instructive example
of using np.ogrid in three dimensions.
1 [xo,yo,zo]=np.ogrid[-1:1:9j, 0:10:5j, 100:200:3j]
2 xo
3 yo
4 zo
5 xo+yo+zo
In this section, we look at how to communicate with humans and other programs and
how to store intermediate results. We can distinguish at least three scenarios and we
consider simple examples of input and output processes for them. The first supposes
that we are given a text file which contains both words and numbers, and we wish to
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
70 NumPy
read in the latter to NumPy arrays. Conversely, we might want to output numbers to a
text file. The second is similar but simpler, in that we want to process just numbers into
and from text files. The third scenario is similar to the second, but for reasons of speed
and economy of space, we wish to use binary files which can be processed by another
NumPy program, possibly on a different platform.
There remains the generalization of dealing with data produced by another program,
perhaps a spreadsheet or a “number crunching” non-Pythonic program. This is dis-
cussed in Section 4.5.
Lines 1–3 generate some artificial data. The Python function open in line 4 creates a file
called q4.txt and opens it for writing.4 See the docstring for further details of the open
function. Within the program, the file has the arbitrary identifier outfile. In line 5, we
write a header string terminated by a newline character n and a second newline to insert
a blank line below the header. Line 6 introduces a new feature, the Python zip function.
This takes a number of iterable objects, here the two NumPy arrays, and returns the next
object from each of them as a tuple. The for loop terminates when the shortest iterable
object is exhausted. Within the loop, we write a formatted string containing elements
from each of the arrays, and terminated with a newline. Finally, in line 9 we close the
file. You can check the correctness of this snippet by looking for the file q4.txt (it
should be in your current directory), and reading it with your favourite text editor.
Now let us consider the reverse process. We have a text file q4.txt, about which
we know the following. The first two lines form a header, which can be discarded.
Next follow an unknown number of lines, which all have the same form: a sequence of
“words” separated by white space. Labelling the words in each line from zero, we wish
to build two NumPy arrays: one containing word 2 as an int and the other word 6 as a
4 The names of files depend strongly on the choice of operating system. The choice here assumes Unix and
creates or overwrites a file in the current working directory. You should replace the first string by the
valid address of the desired file in your operating system.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
4.4 Domestic Input and Output 71
float. (To see why we want words 2 and 6, look at the text file.) The dimension of the
arrays will be the same as the number of lines to be read. The following snippet carries
out this task and, like the previous one, it is mainly pure Python.
1 infile=open("q4.txt","r")
2 lquarter=[]
3 lresult=[]
4 temp=infile.readline()
5 for line in infile:
6 words=line.split()
7 lquarter.append(int(words[2]))
8 lresult.append(float(words[6]))
9 infile.close()
10 import numpy as np
11 aquarter=np.array(lquarter,dtype=int)
12 aresult=np.array(lresult)
13 aquarter, aresult
Line 1 connects the text file to the program for reading, and line 10 eventually discon-
nects it. Lines 2 and 3 create empty lists to hold the numbers. The file object infile is
a list of strings, one per line. Line 4 then reads the first string into a string in memory
with identifier temp, and so effectively deletes the first element of infile. Line 5 re-
peats the process, so we have discarded the header. (For a more verbose header, a for
loop might be more appropriate.) Now we loop through the remaining lines in infile.
Acting on the string line, the string function split breaks it into a list of substrings,
here called words. The breaks occur at each white space inside line and the white
space itself is eliminated. (We could use a different splitting character, e.g., a comma.
See the docstring for details. Now words[2] contains a string which we wish to convert
to an integer, and the int function in line 8 does this. Next we append the integer to
the lquarter list. Line 9 repeats the process for the floating-point number. Finally, in
lines 12 and 13 we construct NumPy arrays from the lists. Note that at no point have we
needed to know how many data lines were present.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
72 NumPy
8 arr[1, : ]=c
9 arr[2, : ]=s
10 arr[3, : ]=t
11 np.savetxt(’x.txt’,x)
12 np.savetxt(’xcst.txt’,(x,c,s,t))
13 np.savetxt(’xarr.txt’,arr)
The first ten lines merely set up some data. Line 11 creates a text file, opens it, copies
the vector x to it using the default format %.18e, and closes the file. Line 12 shows
how to copy many vectors (or arrays), but of equal shapes. We merely parcel them into
a tuple. Line 13 shows how to save an array into a file. For more possibilities, see the
docstring for np.savetxt.
With the current setup, reading the files is easy.
1 xc=np.loadtxt(’x.txt’)
2 xc,cc,sc,tc=np.loadtxt(’xcst.txt’)
3 arrc=np.loadtxt(’xarr.txt’)
There are of course possibilities that are more elaborate. See the docstring for details.
np.save(’array.npy’,arr)
arrc = np.load(’array.npy’)
File opening and closing is handled silently provided we use the .npy postfix in the file
name.
A collection of arrays of possibly varying shapes is handled by creating a zipped
binary archive. Using the array definitions above, we might write the vectors with a one
liner
np.savez(’test.npz’,x=x,c=c,s=s,t=t)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
4.5 Foreign Input and Output 73
1 temp=np.load(’test.npz’)
2 temp.files
3 xc=temp[’x’]
4 cc=temp[’c’]
5 sc=temp[’s’]
6 tc=temp[’t’]
In line 2, we display the names of the files in the archive, which has the identifier temp.
Lines 3–6 show how to recover the arrays. Notice that, in both processes, file opening,
zipping, unzipping and closing are handled silently. There are many other ways of per-
forming binary input/output, in particular the struct module. See the documentation for
details.
We now turn, albeit very briefly, to the problem of how to read in data from a non-Python
source, and the answer depends on the size of the data.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
74 NumPy
(API). There are officially supported interfaces for C, C++, Fortran90 and Java. Clearly,
HDF5 fulfils a need in scientific number crunching! Therefore, the scientific packages
Mathematica, Matlab, R and SciLab offer “third party” APIs. Python is perhaps unique
in that it offers two and one half, aimed at slightly different audiences.
The half refers to a minimalistic interface within Pandas, see the relevant documen-
tation. The package h5py7 offers both low- and high-level access to HDF5, aiming for
very similar functionality to its compiled language API cousins. The other package
PyTables8 offers only a high-level interface, but then provides additional facilities such
as sophisticated indexing and query capabilities that we would expect to find only in a
database.
The HDF5 documentation is not helpful in deciding which Python API to use, and
you really need to study all three of them before making a decision. Note that installation
of a sufficiently up-to-date version of HDF5 on Unix/Linux platforms is not for the faint-
hearted. This is one instance where Windows users get a helping hand in that both h5py
and PyTables offer binary installations, which include a private copy of HDF5.
NumPy contains a number of miscellaneous ufuncs, and we group some of the most
useful ones here. For a more complete review, see the NumPy reference manual (NumPy
Community 2017a).
x=np.array([[5,4,1],[7,3,2]])
np.max(x)
np.max(x,axis=0)
np.max(x,axis=1)
Of course, if one of the elements is a nan, it is automatically the maximum of the array
and any subarrays containing it. The function np.nanmax behaves almost identically,
but ignores nan values. Minima are handled by np.min and np.nanmin. The range
(peak to peak) of the array, or subarray thereof, is given by np.ptp(x). Look at the
function docstrings for further information.
Incidentally, np.isnan(x) returns an array of Booleans of the same shape as x with
a True entry for each instance of a nan. The function np.isfinite(x) does the op-
posite, returning False for each nan or inf.
7 Its website, http://www.h5py.org, offers documentation and downloads.
8 There is a very informative website at http://www.pytables.org.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
4.7 Polynomials 75
np.average(x,axis=None,weights=None)
np.var(x,axis=0,ddof=0)
The first two arguments should be familiar. Note that, if the elements of x are com-
plex, the squaring operation uses a complex conjugate so as to generate a real value.
The computation of the variance involves a division by n, where n is the number of el-
ements involved. If ddof, the “delta degrees of freedom”, is specified, then the divisor
is replaced by n-ddof. The function np.std is very similar and computes the standard
deviation.
NumPy also contains a number of functions for correlating data. The most used ones
are np.corrcoeff, np.correlate and np.cov. Their syntax is rather different from
that of the functions discussed above, so the relevant docstrings need careful study be-
fore use.
4.7 Polynomials
Polynomials in a single variable occur very frequently in data analysis, and NumPy of-
fers several approaches for manipulating them. A concise way to describe a polynomial
is in terms of its coefficients, e.g.,
c0 x4 + c1 x3 + c2 x2 + c3 x + c4 ↔ {c0 , c1 , c2 , c3 , c4 } ↔ c0 , c1 , c2 , c3 , c4
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
76 NumPy
1 import numpy as np
2
3 roots=[0,1,1,2]
4 coeffs=np.poly(roots)
5 print "coeffs = \n", coeffs
6 print "np.roots(coeffs) = \n", np.roots(coeffs)
7 x=np.linspace(0,0.5*np.pi,7)
8 y=np.sin(x)
9 c=np.polyfit(x,y,3)
10 print "c = \n", c
11 y1=np.polyval(c,x)
12 print "y = \n", y, "\n y1 = \n", y1, "\n y1-y = \n", y1-y
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
4.8 Linear Algebra 77
A = np.matrix([[1,2],[3,4]])
b = np.matrix([[5],[6]])
A, b, A*b
Note that what users regard as vectors, e.g., b, have to be recast as matrices. However
quantities such as b*b, A*A and A**17 are readily available.
Other users may prefer to retain the versatility of NumPy arrays, and instead predefine
matrix multiplication via the np.dot function. They would write
A = np.array([[1,2],[3,4]])
b = np.array([[5],[6]])
A, b, np.dot(A,b)
For this simple model equation there is little difference between these two approaches.
However, as we shall see, almost all of the useful functions have been developed for this
second version, so this is the one we recommend.
Suppose that A is a NumPy array with two dimensions. (Note that, unlike the usual
algebra conventions, indices start with zero rather than one.) The transpose of A is avail-
able as A.transpose(), or more succinctly as A.T. Note that NumPy does not distin-
guish between column and row vectors. This means that, if u is a one-dimensional array
or vector, then u.T = u.
We already know how to build zero matrices, e.g., z=np.zeros((4,4)). The func-
tion np.identity creates identity matrices, e.g.,
I=np.identity(3,dtype=float).
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
78 NumPy
Next consider a different situation where we have a set of m vectors v1, v2, . . . , vm
all of length n and we want to construct an m × n matrix with the vectors as rows. This
is accomplished easily with the np.vstack function as the following example shows.
1 v1=np.array([1,2,3])
2 v2=np.array([4,5,6])
3 rows=np.vstack((v1,v2))
4 rows
5 cols=rows.T
6 cols
Note that, because the actual number of arguments in line 3 is variable, we have to wrap
them in a tuple first, because np.vstack takes precisely one argument. We could of
course encode them as columns, creating an n × m matrix, as the last two lines of the
code snippet show.
1 import numpy as np
2 import numpy.linalg as npl
3
4 a=np.array([[4,2,0],[9,3,7],[1,2,1]])
5 print "a = ", a
6 print "det(a) = ", npl.det(a)
7 b=npl.inv(a)
8 print "b = ", b, "\n b*a = ", np.dot(b,a)
The function npl.eig can be used to generate eigenvalues and eigenvectors. This
function delivers the eigenvectors as columns of an n-row matrix. Each column has unit
Euclidean length, i.e., the eigenvectors are normalized.
1 import numpy as np
2 import numpy.linalg as npl
3
4 a=np.array([[-2,-4,2],[-2,1,2],[4,2,5]])
5 evals, evecs = npl.eig(a)
6 eval1 = evals[0]
7 evec1 = evecs[:,0]
8 eval1, evec1
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
4.9 More NumPy and Beyond 79
There are many more functions available in the np.linalg module. Try npl? for de-
tails.
1 import numpy as np
2 import numpy.linalg as npl
3
4 a=np.array([[3,2,1],[5,5,5],[1,4,6]])
5 b=np.array([[5,1],[5,0],[-3,-7.0/2]])
6 x=npl.solve(a,b)
7 x
This is but the briefest introduction to the linear algebra capabilities of NumPy. See
also Section 4.9.1.
The NumPy module contains far more resources than those sketched here, in particular
several specialized groups of functions, including the following.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
80 NumPy
4.9.1 SciPy
The SciPy module scipy contains a wide variety of further specialized groups of func-
tions. A subset of the available groups is listed here.
• scipy.special makes many special functions, e.g., Bessel functions, readily available.
• scipy.integrate contains various quadrature functions and, most importantly, routines
for solving the initial value problems for systems of ordinary differential equations
(see Section 8.1).
• scipy.optimize, which covers optimization and root finding functions, for which a
simple example is given below.
• scipy.fftpack, which contains a more extensive set of routines involving discrete
Fourier transforms.
• scipy.linalg, scipy.sparse and scipy.sparse.linalg, which extend considerably the lin-
ear algebra capabilities of NumPy. They should be faster, and for heavy duty linear
algebra they are to be preferred over their NumPy counterparts.
As a simple example, we address a problem which arises later in Section 8.4.5, to
determine all positive solutions of
coth ν = ν.
A rough sketch of the graphs of each side of the equation as functions of ν shows that
there is precisely one solution, and that it is greater than 1. Using the information we
have gleaned from the previous code snippet, the following code solves the problem.
1 import numpy as np
2 import scipy.optimize as sco
3
4 def fun(x):
5 return np.cosh(x)/np.sinh(x)-x
6
7 roots=sco.fsolve(fun,1.0)
8 root= roots[0]
9 print "root is %15.12f and value is %e" % (root, fun(root))
The required root is approximately 1.199678640258, for which the function value is
about 3 × 10−14 .
Fuller details, including groups not mentioned here, can be found in the reference
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
4.9 More NumPy and Beyond 81
manual, SciPy Community (2017). On the whole, the documentation reaches a high
standard, if not quite the equal of that for NumPy.
4.9.2 SciKits
In addition, there are a number of scientific packages which are not included in SciPy for
various reasons. They may be too specialized for inclusion, they may involve software
licences, e.g., GPL, which are incompatible with SciPy’s BSD licence or quite simply
they reflect work in progress. Many of these can be accessed via the SciKits web page,9
or via the Python Package Index (see Section A.5). We shall demonstrate their usage
with concrete examples. First, in the treatment of boundary value problems for ordinary
differential equations, in Section 8.4, the package scikits.bvp1lg will be needed. In
Section 8.5, which discusses simple delay differential equations, we have chosen not to
use the SciKits package skikits.pydde, because the package pydelay, downloadable
from its web page,10 appears to be both more versatile and user friendly. Installation of
the two packages is identical, and is described in Section A.5.
9 http://scikits.appspot.com/.
10 http://pydelay.sourceforge.net.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.006
5 Two-Dimensional Graphics
5.1 Introduction
The most venerable and perhaps best-known scientific graphics package is Gnuplot, and
downloads of this open-source project can be obtained from its website.1 The official
documentation is Gnuplot Community (2016), some 250 pages, and a more descrip-
tive introduction can be found in Janert (2015). Gnuplot is of course independent of
Python. However, there is a NumPy interface to it, which provides Python-like access
to the most commonly used Gnuplot functions. This is available on line.2 Although
most scientific Python implementations install the relevant code as a matter of course,
the documentation and example files from this online source are useful. For many ap-
plications requiring two-dimensional graphics, the output from Gnuplot is satisfactory,
but only at its best is it of publication quality. Here Matlab has been, until recently,
the market leader in this respect, but Python aims to equal or surpass it in quality and
versatility.
The Matplotlib project3 aims to produce Matlab-quality graphics as an add-on to
NumPy. Almost certainly, this should be part of your installation. It is installed by de-
fault in most Python packages designed for scientists. There is extensive “official docu-
mentation” (2842 pages) at Matplotlib Community (2016), and a useful alternative de-
scription in Tosi (2009). The reader is strongly urged to peruse the Matplotlib Gallery4
where a large collection of publication quality figures, and the code to generate them,
is displayed. This is an excellent way (a) to explore the visual capabilities of Matplotlib
and (b) to obtain code snippets to help create a desired figure. Because Matplotlib con-
tains hundreds of functions, we can include here only a small subset. Note that almost
all of the figures in this and subsequent chapters were generated using Matplotlib, and
the relevant code snippets are included here. However the exigencies of book publishing
have required the conversion of these colour figures to black, white and many shades of
grey.
As with all other powerful versatile tools, a potential user is strongly encouraged to
read the instruction manual, but at well over one thousand pages few scientific users
will attempt to do so. Indeed, the philosophy of this book is “observe and explore”.
1 See http://www.gnuplot.info.
2 See http://gnuplot-py/sourceforge.net.
3 See http://matplotlib.org.
4 See http://matplotlib.org/gallery.html.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
5.2 Getting Started: Simple Figures 83
We have therefore restricted the coverage to what seems to be the most useful for a
scientist. Sections 5.2–5.9 look at a variety of single plots, and Section 5.8 shows how to
display mathematical formulae in a figure. Next, Section 5.10 looks at the construction
of compound figures. Finally, Section 5.11 shows how to construct an intricate figure
pixel by pixel. This is a long chapter. However, spending a few minutes reading the next
section carefully can save prospective but impatient users hours of frustration.
Converting theoretical wishes into actual figures involves two distinct processes known
as “front-ends” and “back-ends”, and the reader needs to know a little about both.
5.2.1 Front-ends
The front-end is the user interface. As a prospective user, you will need to decide
whether you will always want to prescribe figures from within the confines of an in-
terpreter, or whether you want more generality, perhaps developing figures within an
interpreter, but later invoking them by batch-processing a Python file, or even nesting
them within some other application. The first, simpler but more restrictive choice is
catered for by the pylab branch of Matplotlib. You can gain access to it from the com-
mand line by, e.g.,
ipython --pylab
which loads NumPy and Matplotlib implicitly and without the safeguards advocated in
the namespace/module discussion of Section 3.4. The motivation is to attract Matlab
users by providing nearly identical syntax. But scientist Matlab users who are reading
this chapter probably wish to escape its restrictions, so we will eschew this approach.
Instead, we shall follow the pyplot branch, the main topic of this chapter. As we shall
see, this respects the namespace/module conventions and so offers a path to greater
generality.
Working in the IPython interpreter, the recommended way to access pyplot is as fol-
lows:
import matplotlib.pyplot as plt
Next, try some introspection, plt?. Further the query plt.tab will reveal over 200
possibilities, so this is a large module. Fortunately, a wide variety of pictures can be
drawn using surprisingly few commands.
5.2.2 Back-ends
The front-end supplies Python with user-requests. But how should Python turn them into
visible results? Should they be to a screen (which screen?), to paper (which printer?), to
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
84 Two-Dimensional Graphics
another application? These questions are obviously hardware-dependent and it is the job
of the software back-end to answer them. The Matplotlib package contains a wide range
of back-ends, but which one should be used? Fortunately, your Python installer should
have recognized your hardware configuration and should have chosen the optimal gen-
eral purpose back-end for it. For the curious, the command plt.get_backend() will
reveal which back-end is actually being used. In many instances there should be no need
to change this, but see below. If you want to change this, you first need to know what
back-ends are available to you. To find this you need to locate the Matplotlib preferences
file. The commands
import matplotlib
matplotlib.matplotlib_fname()
will reveal its location. Next you open this text file in your editor and navigate to about
line 30. Above this you will see a list of available back-ends. (The peculiar mnemonic
Agg stands for the Anti-Grain Geometry, which is the library5 which produces such
high-quality results.) The names are case-insensitive, so I recommend using lower case.
If you want to change the preinstalled default choice permanently, you can edit the
preferences file to specify the one you want to be used. However, if you merely wish to
change it for the current session, then before issuing any plot commands, invoke, e.g.,
plt.switch_backend(’qtagg’)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
5.2 Getting Started: Simple Figures 85
1 import numpy as np
2 import matplotlib.pyplot as plt
3
11 x=np.linspace(-np.pi,np.pi,101)
12 y=np.sin(x)+np.sin(3*x)/3.0
13
14 plt.plot(x, y)
15
16 plt.xlabel(’x’)
17 plt.ylabel(’y’)
18 plt.title(’A simple plot’)
19
20 #plt.show()
21 plt.savefig(’foo.pdf’)
Lines 1–2 and 11–12 should be familiar. As we suggested in Section 5.2.1, line 2 shows
the recommended way to import Matplotlib for general use.
The single line 14 should construct a figure, lines 16–18 decorate it and line 21 saves
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
86 Two-Dimensional Graphics
it as a file in the current directory. (The types of file which can be written depend on the
implementation and back-end. Most support png, pdf, ps, eps and svg files, and your
implementation should support other formats.) We then have two choices as to how to
display it. For developing a figure, and for use in the interpreter terminal, line 5 toggles
“interactive mode” on (and the less frequently used plt.ioff switches it off). Then, as
we add further lines, their effect is displayed instantly. This can absorb resources, and
the default is to make changes but not display them. Finally, when the figure is ready to
be displayed, and you are not using the interpreter terminal, the commented-out line 20
displays the completed figure. Thus, when using the IPython interpreter in terminal
mode, you need to invoke either plt.ion() or plt.show() to send any output to the
screen. Within the interpreter, they give identical outputs, but plt.show has a blocking
property, and further progress is halted until the picture is deleted. You need to find
by experiment which command best suits your work style. The figure itself is plain and
unadorned, although the default axis ranges look reasonable. In Section 5.4, we shall see
how to enhance it. It is remarkable, though, that we need only three Matplotlib functions
to draw, display and save a figure! Do use introspection to see, in more detail, what all
of the functions used in the snippet can actually do. Also try changing the labels or title,
lines 16–18 of the code, and observe what happens in the window.
Next consider developing figures in the IPython notebook. First copy the code snippet
into a notebook cell. Next comment out line 5 of the snippet, and uncomment line 7.
Now execute it via shift+return. You should obtain identical results.
Finally, reset the kernel, comment out line 7 and uncomment line 9. This enables both
interactivity and the nbagg back-end, and so draws the figure with the notebook, rather
than creating an external window. However, the interactivity is maintained.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
5.3 Object-Oriented Matplotlib 87
browsing down and up the stack of modified figures. Button 6 allows control of the
margins of the figure. In principle, button 7 allows you to save to file the current figure.
This option is implementation-dependent, and will not function in all installations.
Figure 5.2 A schematic figure produced by Matplotlib. The totality is a Figure class instance. It
contains two Axes class instances. Each instance contains an x-axis and a y-axis.
Matplotlib is capable of very much more than the simple plot discussed above. This is
implemented in object-oriented fashion using classes, as reviewed briefly in Section 3.9.
However, while that section was concerned with the actual construction of classes, the
scientist user will be using predefined ones, and so needs only to assimilate two pieces
of jargon.
A Figure class instance is what an artist would call his “canvas”, see Figure 5.2. A
Matplotlib session might include several Figures. Within a Figure there may be one or
more class Axes instances. Note that “axes” is a plural noun. It refers to, e.g., the set
of coordinate axes. For example, Figure 5.2 contains two Axes instances. Each instance
contains an x-axis and a y-axis. In order to master plotting, we need to be able to access
both classes.
Fortunately it is very easy to construct both Figure and Axes instances. Each Figure
instance can be initiated by a call of plt.figure(), e.g.,
Clearly an Axes instance belongs to a Figure. However, a Figure may contain many
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
88 Two-Dimensional Graphics
Axes. For example, Figure 5.2 contains two Axes arranged in one row with two columns.
Suppose, more generally, we want six instances in fig2, arranged as two rows and three
columns. Then the command to create instance number 5 (bottom row, middle column)
is
ax5 = fig2.add_subplot(2, 3, 5)
or, provided all numbers are less than ten, the useful abbreviation,
ax5 = fig2.add_subplot(235)
As an example, let us consider the simplest case, and rewrite the code snippet from
the previous section in object-oriented form. Lines 14–18 and 21 should be replaced
by
1 fig = plt.figure()
2 ax = fig.add_subplot(111)
3 ax.plot(x, y)
4 ax.set_xlabel(’x’)
5 ax.set_ylabel(’y’)
6 ax.set_title(’A simple plot’)
7
8 fig.savefig(’foo.pdf’)
“Why should I?” the tyro will exclaim, “why should I replace four valid lines of
code with six, and, change the function names?” We need to examine carefully what
is going on here. In practice Matplotlib treats the earlier snippet as follows. It gener-
ates automatically instructions equivalent to lines 1–2 above, but the private names for
the fig and ax are not available to the ordinary user. Next it defines plt.plot() to
be the same as ax.plot. So far so good. But it then defines plt.xlabel() to mean
ax.set_xlabel(), and similarly for the other two decorators! This change of name,
made to ensure Matlab consistency, is harmless until one tries to construct more com-
plicated figures. Unless one adopts the object-oriented approach, one is forced to learn
two sets of commands. It is for this reason that I recommend using the object-oriented
procedure for anything other than quick simple figures. The sceptical reader might like
to do a little introspection. Typing plt. plus tab reveals (at the time of writing) 232
possibilities, while ax. plus tab produces 317.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
5.4 Cartesian Plots 89
the default style options. The most concise call would be ax.plot(y), where y is a
NumPy vector of length n. Then a default x vector is created with integer spacing to
enable the curve to be drawn. The syntax allows many curves to be drawn with one call.
Suppose x, y and z are vectors of the same length. Then ax.plot(x,y,x,z) would
produce two curves. However, I recommend that you usually draw only one curve per
call, because it is then much easier to enhance individual curves with a call of the type
ax.plot(x,y,fmt), where the format parameter(s) fmt is about to be described.
Table 5.1 The standard Matplotlib colour choices. If the colours of a set of curves are not
specified, then Matplotlib cycles through the first five colours.
Character Colour
b blue (default)
g green
r red
c cyan
m magenta
y yellow
k black
w white
By the style of a curve, we mean its colour, nature and thickness. Matplotlib allows
a variety of descriptions for colour. The most-used ones are given in Table 5.1. Thus,
if x and y are vectors of the same length, then we could draw a magenta curve with
ax.plot(x,y,color=’magenta’), or, more concisely, via ax.plot(x,y,’m’). If
several curves are drawn and no colours are specified, then the colours cycle through
the first five possibilities listed above.
Character(s) Description
- solid curve (default)
-- dashed curve
-. dash–dot curve
: dotted curve
Curves are usually solid lines, the default, but other forms are possible, see Table 5.2.
The clearest way to use these is to concatenate them with the colour parameter, e.g.,
’m-.’ as a parameter produces a magenta dash–dotted curve. The case where we want
no curve at all is dealt with below.
The final line style is width, measured by float values in printer’s points. We could
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
90 Two-Dimensional Graphics
use, e.g., linewidth=2 or, more concisely, lw=2. Thus, to draw a magenta dash–dotted
curve of width four points, we would use
ax.plot(x,y,’m-.’,lw=4)
Character Colour
. point (default)
o circle
* star
+ plus
x x
v triangle down
ˆ triangle up
< triangle left
> triangle right
n square
p pentagon
h hexagon
Matplotlib offers a menagerie of marker styles, 22 at the last count, and the 12 most com-
monly used ones are given in Table 5.3. For a complete list, see the ax.plot docstring.
Again the easiest way to use is them is by concatenation, e.g., ’m-.x’ as a parame-
ter produces a magenta dash–dotted curve with “x” markers. Notice that the parameter
’mo’ produces magenta circle markers at each of the points, but no curve joining them.
Thus invoking ax.plot(x,y,’b--’) followed by ax.plot(x,y,’ro’) will draw a
blue dashed curve with red circle markers. We can also control the colour and size with
parameters that are more verbose, e.g.,
ax.plot(x,y,’o’,markerfacecolor=’blue’,markersize=2.5)
This form is usually used if we make two-tone markers with a different colour edge, e.g.,
by adding the parameters markeredgecolor=’red’,markeredgewidth=2. There are
many, many possibilities, not all of them aesthetically pleasant.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
5.4 Cartesian Plots 91
ax.set_xlim(xmin,xmax)
and similarly for the y-axis. The functions ax.set_xticks and ax.set_yticks con-
trol the ticks along the axes, and their docstrings should be consulted if you wish to
change the default settings.
By default, Matplotlib does not include a grid. It can be added by using ax.grid().
This function has many optional parameters and, if you wish to fine tune the grid, pe-
rusal of the docstring is recommended.
Suppose we are producing a figure with two or more curves. We may attach a label to
any curve, by including the parameter label=’string’ in the call to ax.plot. After
all of the curves have been plotted, the function ax.legend(loc=’best’) draws a box
in the most suitable position. Inside the box, there will be a line for each labelled curve
showing its style and label. As usual, the function can take a variety of parameters.
Consult the docstring for details.
The title of a plot and its axes are most conveniently labelled with the instructions
ax.set_title(), ax.set_xlabel() and ax.set_ylabel(), where the functions
expect at least a string argument. You can use introspection to find additional possible
arguments. If you are drawing several graphs in the same figure (see Section 5.10), then
ax.set_title() is used to label each of them, while fig.suptitle() generates an
overall figure title.
4 x=np.linspace(-np.pi,np.pi,101)
5 f=np.ones_like(x)
6 f[x<0]=-1
7 y1=(4/np.pi)*(np.sin(x)+np.sin(3*x)/3.0)
8 y2=y1+(4/np.pi)*(np.sin(5*x)/5.0+np.sin(7*x)/7.0)
9 y3=y2+(4/np.pi)*(np.sin(9*x)/9.0+np.sin(11*x)/11.0)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
92 Two-Dimensional Graphics
10
11 plt.ion()
12 fig = plt.figure()
13 ax = fig.add_subplot(111)
14
15 ax.plot(x,f,’b-’,lw=3,label=’f(x)’)
16 ax.plot(x,y1,’c--’,lw=2,label=’two terms’)
17 ax.plot(x,y2,’r-.’,lw=2,label=’four terms’)
18 ax.plot(x, y3,’b:’,lw=2,label=’six terms’)
19 ax.legend(loc=’best’)
20 ax.set_xlabel(’x’,style=’italic’)
21 ax.set_ylabel(’partial sums’,style=’italic’)
22 fig.suptitle(’Partial sums for Fourier series of f(x)’,
23 size=16,weight=’bold’)
Several points need to be made about the decorations. Although it is more appropriate
for a presentation than a book figure, we have included a title in a larger and bolder font.
The axis labels have been set in italic font. More sophisticated textual decorations are
possible in Matplotlib, and they will be discussed in Section 5.8.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
5.5 Polar Plots 93
Suppose we use polar coordinates (r, θ) and define a curve by r = f (θ). It is straightfor-
ward to plot this using plt.polar, which behaves rather like ax.plot. However, there
are some significant differences, so perusal of the docstring before use is recommended.
Figure 5.4 was created using the following snippet. (Remember to include one of the
three alternatives from Section 5.2.3.)
1 import numpy as np
2 import matplotlib.pyplot as plt
3 theta=np.linspace(0,2*np.pi,201)
4 r1=np.abs(np.cos(5.0*theta) - 1.5*np.sin(3.0*theta))
5 r2=theta/np.pi
6 r3=2.25*np.ones_like(theta)
7
8 fig = plt.figure()
9 ax = fig.add_subplot(111,projection=’polar’)
10
11 ax.plot(theta, r1,label=’trig’)
12 ax.plot(5*theta, r2,label=’spiral’)
13 ax.plot(theta, r3,label=’circle’)
14 ax.legend(loc=’best’)
The interactive pan/zoom button behaves differently when applied to polar plots. The
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
94 Two-Dimensional Graphics
radial coordinate labels can be rotated to a new position using the left mouse button,
while the right button zooms the radial scale.
When measurements are involved, we often need to display error bars. Matplotlib han-
dles these efficiently using the function ax.errorbar. This behaves like ax.plot, but
with extra parameters. Consider first errors in the y-variable, which we specify with the
yerr variable. Suppose the length of y is n. If yerr has the same dimension, then sym-
metric error bars are drawn. Thus for the kth point at y[k] the error bar extends from
y[k]-yerr[k] to y[k]+yerr[k]. If the errors are not symmetric, then yerr should be
a 2×n array, and the kth error bar extends from y[k]-yerr[0,k] to y[k]+yerr[1,k].
Analogous remarks apply to x-errors and the variable xerr. By default the colour and
line width are derived from the main curve. Otherwise, they can be set with the param-
eters ecolor and elinewidth. For other parameters, see the ax.errorbar docstring.
The thoroughly artificial errors shown in Figure 5.5 were produced using the follow-
ing code snippet.
1 import numpy as np
2 import numpy.random as npr
3 x=np.linspace(0,4,21)
4 y=np.exp(-x)
5 xe=0.08*npr.randn(len(x))
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
5.7 Text and Annotations 95
6 ye=0.1*npr.randn(len(y))
7
10 fig = plt.figure()
11 ax = fig.add_subplot(111)
12 ax.errorbar(x,y,fmt=’bo’,lw=2,xerr=xe,yerr=ye,
13 ecolor=’r’,elinewidth=1)
Suppose we wish to place a plain text string in a figure starting at (x, y) in user coordi-
nates, i.e., those defined by the figure axes. The extremely versatile matplotlib function
ax.text(x,y,’some plain text’) does precisely that. There are various ways of
enhancing it, colours, boxes etc., which are described in the function’s docstring.
Sometimes we wish to refer to a particular feature on the figure, and this is the purpose
of ax.annotate, which has a slightly idiosyncratic syntax, see the snippet and the
function’s docstring.
1 import numpy as np
2 x=np.linspace(0,2,101)
3 y=(x-1)**3+1
4
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
96 Two-Dimensional Graphics
7 fig = plt.figure()
8 ax = fig.add_subplot(111)
9 ax.plot(x,y)
10 ax.annotate(’point of inflection at x=1’,xy=(1,1),
11 xytext=(0.8,0.5),
12 arrowprops=dict(facecolor=’black’,width=1,
13 shrink=0.05))
This produces Figure 5.6. For examples of annotations which are more sophisticated,
see the Matplotlib gallery referred to at the start of this chapter.
It is an opportune moment to discuss the Achilles heel of all plotting software. How do
we display decently formatted mathematical formulae? Most word processors offer add-
on tools to display mathematical formulae but quite often, and especially for intricate
formulae, they look ugly. Mathematicians have learned how to produce book quality
displays using the LATEX package.6 Indeed, this book was produced using it.
First a few words about LATEX. The quasi-official and most comprehensive package is
TexLive.7 This is available with extensive documentation and ready-to-run binaries for
almost any platform and includes all commonly used LATEX programs, macro packages
and fonts. Many other less comprehensive collections are available.
However, Matplotlib has made a very brave effort to make TEX-like features available
for both non-LATEX and LATEX users. For small-scale one-off use, this can be highly
effective.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
5.8 Displaying Mathematical Formulae 97
To understand this line, consider first the string itself. You need to know that mathe-
matics is defined in TEX between a pair of dollar ($) signs. Then TEX strips the dollar
signs and sets the mathematics (by default) in Computer Modern Roman (CMR) italic
font with the font size determined by the (TEX) context. The r before the first string
delimiter indicates that this is a raw string, so that Matplotlib will call up its mathtext
engine. (Without it, the dollar signs would be rendered verbatim.) The code line above
is used in the first snippet in Section 5.9, producing Figure 5.7. The visual aspect is
grotesque! The four words are rendered in a sans-serif font of the Matplotlib default
size. The mathematics is rendered immaculately, but in CMR italic font at the TEX de-
fault size, which is much smaller! Apart from the font mismatch, there would appear to
be no easy way to resolve the size discrepancy.
Fortunately, TEX can come to the rescue. One obvious solution to the font/size dis-
parity is to render the entire string in TEX mathematics mode. We want the first four
words in the default TEX font, though, and we can achieve this by enclosing them in
braces preceded by \rm. However, you need to know that TEX mathematics mode gob-
bles spaces, so you need to insert \ or \, or \; to obtain small, medium or larger
spacing. Our revised command is
ax.set_title(r’${\rm The\ level\ contours\ of\;} z=xˆ2-yˆ2$’,
fontsize=20)
The code line above is used in the second snippet in Section 5.9, leading to Figure
5.8. The font/size mismatch is removed, and the result scales consistently. (In fact, this
book has been typeset via LATEX using the Times family of fonts as the default, and TEX
has been told to respect this. Thus the formulae in the titles to Figures 5.7 and 5.8 are
displayed in different fonts). Do not forget that the same treatment can be applied to all
of the other text instances that might occur in a figure.
For another realistic example, see Section 8.6.4.
rc(’font’,family=’serif’)
rc(’text’,usetex = True)
(For other possibilities, consult the rc docstring.) Next create the title with
ax.set_title(r’The level contours of $z=xˆ2-yˆ2$’,fontsize=20)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
98 Two-Dimensional Graphics
where the string contains regular LATEX syntax. This is used to create the title in Figure
5.9.
Suppose we have a relation F(x, y) = z, and let z0 be a fixed value for z. Subject to
8 It can be downloaded from http://www.chachatelier.fr/latexit/.
9 Its website is http://klatexformula.sourceforge.net.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
5.9 Contour Plots 99
the conditions of the implicit function theorem, we can, after possibly interchanging
the rôles of x and y, solve this relation, at least locally, for y = f (x, z0 ). These are the
“contour curves” or “contours” of z. What do they look like as z0 varies? In principle,
we need to specify two-dimensional arrays of equal shape for each of x, y and z, and a
vector of values for z0 . Matplotlib allows for a number of shortcuts in this process. For
example, according to the documentation we can omit the x- and y-arrays. However,
Matplotlib then creates the missing arrays as integer-spaced grids using np.meshgrid,
which creates the transpose of what is usually needed, and so we should be careful if
using this option! If we do not specify the z0 vector, we can give instead the number
of contour curves that should be drawn, or accept the default value. By default, the z0
values are not shown and so the plt.clabel function can be used to generate them, us-
ing as argument the value returned by plt.contour. The docstrings of these functions
will reveal further options. Figure 5.7 was produced using the following snippet.
1 import numpy as np
2 import matplotlib.pyplot as plt
3
4 fig = plt.figure()
5 ax = fig.add_subplot(111)
6
7 [X,Y] = np.mgrid[-2.5:2.5:51j,-3:3:61j]
8 Z=X**2-Y**2
9
10 curves=ax.contour(X,Y,Z,12,colors=’k’)
11 ax.clabel(curves)
12
We have seen the ideas behind lines 1–8 before. X, Y and Z are 51 × 61 arrays of float.
In line 10, the function ax.contour attempts to draw 12 contour curves. Its return
value is the set of curves. Then in line 11 we attach Z-value labels to the curves. With
the standard defaults, Matplotlib will display the curves using a rainbow of colours. In
this particular case, they do not reproduce well in this monochrome book. The option
colors=’k’ in line 10 ensures that they all appear in black (the colour corresponding
to ’k’, see Section 5.4.2). There are lots of other options available. The two function
docstrings list the details. Finally, line 13 creates a title according to the first prescription
in Section 5.8.1.
An alternative view is provided by filling the spaces between the contour lines with
colour, and supplying a colour bar. For example, Figure 5.8 was created by replacing
lines 10 and 11 in the snippet above by
im=ax.contourf(X,Y,Z,12)
fig.colorbar(im,orientation=’vertical’)
and the second prescription in Section 5.8.1 was used to replace line 13 generating the
title.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
100 Two-Dimensional Graphics
3
1
0
0
−3
−1
−2 −6
−3 −9
−2 −1 0 1 2
4
10
2
20
30
−2
40 −4
−6
50
−8
60
0 10 20 30 40 50
Finally, we can construct a third view, shown as Figure 5.9, with continuous rather
than discrete colours. This was produced by simply replacing the first line of the amend-
ment above by
im=ax.imshow(Z)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
5.10 Compound Figures 101
We also used the prescription from Section 5.8.2 to generate the title. When using the
ax.imshow option, we need to bear in mind that np.meshgrid is used with default
integer grids of the appropriate size. In order to preserve the spatial information, we may
need to alter the extent and/or aspect parameters. The docstring contains the details.
Also an array transpose may be needed in order to alleviate the meshgrid problem.
We saw in Figure 5.3 how we could present a great deal of information in a single
figure. In many cases, this is not the optimal approach. In order to avoid unnecessary
nomenclature, we introduce a commonplace paradigm. A creative artist would choose
to use either one or the other or a combination of two strategies: start again on additional
new pages of the sketching block, or place several smaller figures on the same page. The
first strategy is conceptually the simpler and so we treat its Matplotlib analogue first.
1 import numpy as np
2 import matplotlib.pyplot as plt
3
4 x=np.linspace(0,2*np.pi,301)
5 y=np.cos(x)
6 z=np.sin(x)
7
8 plt.ion()
9 fig1 = plt.figure()
10 ax1 = fig1.add_subplot(111)
11 ax1.plot(x,y) # First figure
12 fig2 = plt.figure()
13 ax2 = fig2.add_subplot(111)
14 ax2.plot(x,z) # Second figure
Note that this snippet creates two Matplotlib windows. In most installations, the second
will be superposed on the first, and so needs to be dragged aside so that both are visible.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
102 Two-Dimensional Graphics
Sometimes there is a need to present several plots in the same figure, i.e., a compound
figure. In many plotting packages, this is an onerous, complicated procedure. However,
using Matplotlib’s object-oriented features, this task is performed easily. We present
here a totally artificial example, showing four different decay rates in the same figure.
Obviously, the code snippet needed to produce Figure 5.10 needs to be longer than
usual.
1 import numpy as np
2 import matplotlib.pyplot as plt
3
4 x=np.linspace(0,5,101)
5 y1=1.0/(x+1.0)
6 y2=np.exp(-x)
7 y3=np.exp(-0.1*x**2)
8 y4=np.exp(-5*x**2)
9
10 fig=plt.figure()
11 ax1=fig.add_subplot(2,2,1)
12 ax1.plot(x,y1)
13 ax1.set_xlabel(’x’)
14 ax1.set_ylabel(’y1’)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
5.11 Mandelbrot Sets: A Worked Example 103
15 ax2=fig.add_subplot(222)
16 ax2.plot(x,y2)
17 ax2.set_xlabel(’x’)
18 ax2.set_ylabel(’y2’)
19 ax3=fig.add_subplot(223)
20 ax3.plot(x,y3)
21 ax3.set_xlabel(’x’)
22 ax3.set_ylabel(’y3’)
23 ax4=fig.add_subplot(224)
24 ax4.plot(x,y4)
25 ax4.set_xlabel(’x’)
26 ax4.set_ylabel(’y4’)
27 fig.suptitle(’Various decay functions’)
Consider the code snippet. Lines 4–8 construct four vectors of data. Next we need to
introduce some slightly unfamiliar notation. Readers who have mastered Section 3.9
will recognize what is going on here. First, we construct in line 10 a “Figure class
object” using the plt.figure function, and have given the object the name fig.
If you look at Figure 5.10, you will see that the figure comprises a title and four sub-
plots. Let us deal with the title first. It is constructed in line 27 of the snippet using the
suptitle function associated with the class instance fig. Now for the four subplots.
We construct the first of these using line 11. The function fig1.add_subplot(2,2,1)
takes into account that there will be an array of subplots with two rows and two columns.
The final argument notes that we are dealing with the first of these. The return value of
that function is an “Axes subplot class object” with the identifier ax1. If you try ax1.tab
within IPython, you will discover that over 300 possible functions are available, and
three of them are used to plot the curve and label the axes in lines 12–14. Lines 15–26 re-
peat the process for each of the other three plots. Provided that the total number of plots
is less than ten (the usual case), there is a standard abbreviation used in, e.g., line 15,
where add_subplot(2,2,2) has been shortened to add_subplot(222). There is a
myriad of other features which can be used, and a careful perusal of the various function
docstrings will reveal them. Note in particular that ax1.set_title(string) prints a
title relevant to that subplot.
When constructing elaborate compound figures, it is important to keep in mind the
limitations of space. If subplots start overlapping, we should consider invoking the func-
tion fig.tight_layout.
We finish this chapter with a somewhat longer example to show how simple Python
commands can push at the limits of image-processing. Since we have not yet consid-
ered the numerical solution of differential equations, we (artificially) restrict ourselves
to discrete processes. My chosen example, while relatively simple to present, has ex-
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
104 Two-Dimensional Graphics
tremely complicated dynamics and the challenge is to represent them graphically. Al-
though the reader may have no professional interest in Mandelbrot sets, the discussion
of their implementation raises a number of technical points that are more general, e.g.,
• performing operations hundreds of millions of times efficiently,
• removing points dynamically from multi-dimensional arrays,
• creating high-definition images pixel by pixel.
This is why the example was chosen.
Peitgen and Richter (1986) drew attention to the utility of computer graphics for
giving insight into fractals. There are many sites on the internet drawing attention both
to the remarkable figures which can illustrate the boundary of the Mandelbrot set, the
best-known fractal, and to the programs which generate them. Although programs as
short as 100–150 characters exist, the aim of this section is to build on what has been
introduced so far and show how to create a program which produces insightful images
quickly. We first describe very briefly the underlying mathematics, and then discuss an
algorithm to display the set boundary. Finally, we explain how the code snippet below
implements the algorithm.
We shall describe the complex plane in terms of cartesian coordinates x, y or in terms
of the complex variable z = x + iy, where i2 = −1. We need also a map z → f (z) of the
complex plane to itself. Mandelbrot chose f (z) = z2 + c, where c is a constant, but many
other choices are possible. An iterated sequence is then defined by
zn+1 = z2n + c, with z0 given, and n = 0, 1, 2, . . . (5.1)
It is customary to choose z0 = 0, but by a trivial renumbering we choose z0 = c. The
equation (5.1) then defines zn = zn (c).
Here are two examples, corresponding to c = 1 and c = i respectively:
zn = {1, 2, 5, 26, 677, . . .}, zn = {i, −1 + i, −i, −1 + i, −i, . . .}.
We focus attention on the behaviour of zn (c) as n → ∞. If |zn (c)| remains bounded, we
say that c lies in the Mandelbrot set M. Clearly, c = i is in M, but c = 1 is not. A deep
result is that M is a connected set, i.e., it possesses an inside and outside, and we have
exhibited elements of both.
Recall the triangle inequality: if u and v are complex numbers, then |u + v| |u| + |v|.
Using this result, it is straightforward to show that if |c| 2 then zn (c) → ∞, so that c
lies outside M. Now suppose that |c| < 2. If we find that |zN (c)| > 2 for some N, then
the triangle inequality can be used to show that |zn (c)| increases as n increases beyond
N and so |zn (c)| → ∞ as n → ∞. If c lies outside M, there will be a smallest such N,
and, for the purposes of our algorithm, we shall call it the escape parameter (c). The
parameter is not defined if c lies in M, but it is convenient to say (c) = ∞ in this case.
We can carry out an analogous study for Julia sets. Recall that we iterated the recur-
rence relation (5.1) for fixed z0 = 0 with the parameter c varying in order to obtain the
Mandelbrot set. Now consider the alternative approach holding the parameter c fixed
and varying the starting point z0 . Then, if |zn (c)| remains bounded, we say that z0 lies in
the Julia set J(c).
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
5.11 Mandelbrot Sets: A Worked Example 105
The role of the computer is to help to visualize (c) when c lies in some domain in the
complex plane. It turns out that this function has extremely complicated behaviour and
so surface plots are inappropriate. Because structure is observed to exist on all scales,
contour plots are misleading. Instead, we use colour to represent the value of the escape
parameter, and this can be done on a pixel-by-pixel basis. This creates the figures which
have captured the imagination of so many. Here we explore the classic visualization of
the Mandelbrot set. Later, in Section 6.10, we look at an alternative view of the Julia set
for the process (5.1).
We first choose a rectangular domain xlo x xhi , ylo y yhi . We know how
to construct x and y on an evenly spaced rectangular grid using np.mgrid, and we
can then build the parameter c = x + 1j*y at each grid point. Next, for each grid
point we make z a copy of c. We need to specify an integer parameter max_iter, the
maximum number of Mandelbrot iterations that we are prepared to carry out. We then
construct an iteration loop to evolve z to z**2+c. At each stage of the loop, we test
whether np.abs(z)>2. The first time this happens we fix the escape parameter eps
to be the iteration number. If this never happens, we set eps to max_iter. We then
proceed to the next grid point. At the end of this process, we have a rectangular array of
escape parameters that we can dispatch for visualization. This is straightforward. There
is, however, an issue which we need to take into account. The grid dimensions will be
> 103 in each direction, and max_iter > 102 . Thus we are looking at a minimum of
108 Mandelbrot iterations! We need to vectorize this calculation. In principle, this too is
simple. We use a for loop to carry out the Mandelbrot iteration and use NumPy arrays
to evolve all of the grid points simultaneously. However, if, early on, we set the value
of the escape parameter at a grid point, then we do not want to include that point in
subsequent iterations.
This involves logical operations, and, with care, the discussion of logical operations
on vectors in Section 4.1.5 can be extended to multi-dimensional arrays. However, the
removal of points from a two-dimensional array is non-trivial in comparison with its
one-dimensional analogue. We therefore need to “flatten” our two-dimensional arrays
to produce one-dimensional vectors. We take the z-array and use the reshape function
to construct a vector containing the same points. This is done by pointers. There is no
actual copying of the data. After we have iterated the vector, we check for “escaped
points”, i.e., those for which |z| > 2. For all escaped points, we write simultaneously the
escape parameter. Next we form a new vector with the escaped points deleted. Again
this can be done with no actual copying. Then we iterate the smaller vector and so on.
This will be very fast, but there is a complication. We need to build a two-dimensional
array of escape parameters, and this positional information has been lost in the flattened
truncated vector. The solution is to create a pair of “index vectors” carrying the x- and
y-positions and to truncate them in exactly the same way as we truncate the z-vector.
Thus for any point in the z-vector we can always recover its coordinates by looking at
the corresponding points in the two index vectors.
The code snippet below can be divided into five sections. The first, lines 5–9, is
simple, because we merely set the parameters. (If we were to recast the operation as a
function, these would be the input arguments.)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
106 Two-Dimensional Graphics
The second, lines 12–15, sets up the arrays using what should be familiar func-
tions. The array esc_parms needs some comment though. The convention in image-
processing is to reverse the order of x- and y-coordinates (effectively a transpose) and
for each point we need a triple of unsigned integers of length 8 which will hold the red,
green, blue (rgb) data for that pixel.
Next, in lines 18–21 of the snippet, we “flatten” the arrays for ix, iy and c using the
reshape function. No copying occurs and the flattened arrays contain the same number
of components as their two-dimensional analogues. At this stage, we introduce in line 22
a z-vector which, initially, is a copy of the c-vector, the starting point for the iteration.
1 import numpy as np
2 from time import time
3
17 # Flattened arrays
18 nxny=nx*ny
19 ix_f=np.reshape(ix,nxny)
20 iy_f=np.reshape(iy,nxny)
21 c_f=np.reshape(c,nxny)
22 z_f=c_f.copy() # the iterated variable
23
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
5.11 Mandelbrot Sets: A Worked Example 107
35 esc_parm[iy_f[escape],ix_f[escape],:]=r, g, b
36 escape=-escape # points not escaping
37 # Remove batch of newly escaped points from flattened arrays
38 ix_f=ix_f[escape]
39 iy_f=iy_f[escape]
40 c_f=c_f[escape]
41 z_f=z_f[escape]
42
47 picture=Image.fromarray(esc_parm)
48 picture.show()
49 picture.save("mandelbrot.jpg")
The for loop, lines 24–41, carries out the Mandelbrot iteration. We first test whether
the z-vector is empty, i.e., all of the points have escaped, and if so we halt the iteration.
In lines 28 and 29 we choose a triple of rgb values given by an arbitrary encoding of
the iteration counter. (This can be replaced by another of your choice.) Lines 31 and
32 carry out the iteration. It would have been simpler and clearer to have used one
line, z_f=z_f*z_f+c_f. However, this involves creating two temporary arrays, which
are not needed in the version shown. This version runs in 80% of the time taken by
z_f=z_f*z_f+c_f. Line 33 tests for escaping. escape is a vector of Booleans of the
same size as z_f, with the value True if abs(z_f)>2 and False otherwise. The next
line writes the rgb data for those components where escape is True. It is at this point
that we need the ix_f and iy_f vectors which carry the positions of the points.
As we can readily check, if b is a vector of Booleans, then -b is a vector of the
same size with True and False interchanged. Thus, after line 36, escape is True for all
points except the escaped ones. Now lines 38–41 remove the escaped points from the
arrays ix_f, iy_f, c_f and z_f, after which we traverse the loop again. Note that, if a
point has not escaped after max_iter iterations, its rgb value is the default, zero, and
the corresponding colour is black.
The final part of the code, lines 45–49, invokes the Python Imaging Library (PIL) to
convert the rgb array to a picture, display it and save it as a file. PIL should have been
included in your Python installation package. If not, it can be downloaded together with
documentation from its website.10
This snippet does not use Matplotlib, but, if you wish to embed the picture in a note-
book, then you need to add the following lines to the end of the snippet.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
108 Two-Dimensional Graphics
pil_im=Image.open("mandelbrot.jpg", ’r’)
plt.imshow(np.asarray(pil_im))
The black and white version of the output of the code snippet above is shown as
Figure 5.11. It is worthwhile experimenting with other smaller domains in the complex
plane, in order to appreciate the richness of the Mandelbrot set boundary. You might
prefer a less sombre colour scheme, and it is easy to change line 29 of the snippet to
achieve this.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.007
6 Multi-Dimensional Graphics
6.1 Introduction
Both the screen of a VDU and a paper page are two-dimensional, so any representation
of a three- or higher-dimensional object must ultimately be reduced to two dimensions.
There are (at least) two standard processes to achieve this. In order to avoid too much
abstraction in the presentation, let us assume that the object has been described in terms
of coordinates (x, y, z, w, . . .).
The first reduction process, often called the “cut plane” technique, is to impose some
arbitrary condition or conditions on the coordinates so as to reduce the dimensionality.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
110 Multi-Dimensional Graphics
The simplest and most common approach is to assume constant values for all but two of
the coordinates, e.g., z = z0 , w = w0 , . . . , thus giving a restricted but two-dimensional
view of the object. By doing this several times for a (not too large) set of choices for
z0 , w0 , . . . , henceforth called parameters, we should be able to recover valuable infor-
mation about the object. An example of this approach is the contour curve procedure in
Matplotlib mentioned in the previous section.
The second common reduction process is projection. Suppose first that the object
is three-dimensional. Suppose we choose an “observer direction”, a unit vector in the
three-dimensional space, and project the object onto the two-plane orthogonal to the
observer direction. (If distinct object points project to the same image point we need
to consider whether, and if so how, we want to distinguish them. This difficult issue
is being ignored in this introduction.) In the standard notation for three-dimensional
vectors, let n be the observer direction with n · n = 1. Let x be an arbitrary three-vector.
Then the projection operator P is defined by
P(x) = x − (x · n)n,
and it is easy to see that this vector is orthogonal to n, i.e., n · P(x) = 0 for all x. In phys-
ical terms, the projection onto the plane spanned by the P(x) is what a distant observer
would see. The projection technique also works in higher dimensions, but the physical
interpretation is not as direct. We can hope to “visualize” the object by choosing enough
directions.
Another point which should be noted is that, if an object is to be resolved with N
points per dimension and there are d dimensions, the number of points needed will
be N d . Thus, if d 3, we cannot expect to achieve resolutions as fine as in the two-
dimensional case. Further, there could be a large amount of data, and we may need
some sophisticated software to manage it.
There are two tasks here which need to be considered separately, although the first is
often glossed over.
In principle we may need to convert the data to one of the more-or-less standard data
formats. Perhaps the best known of these are the HDF (Hierarchical Data Format)1
and the VTK (Visualization Tool Kit),2 although there are plenty of others in the public
domain. These formats aim at providing maximum generality in their descriptions of
“objects” to be visualized, and so come with a fairly steep learning curve. The choice
of a particular format is usually predicated by a choice of visualization package.
The task of a visualization package is to take data in a specified format and produce
two-dimensional views. Packages must be fast, so that, irrespective of whether cut plane
or projection techniques are used, figures can be produced and altered with a minimal
time lag. Given that they also need to produce a variety of outputs, images, movies etc.,
1 For more information see https://www.hdfgroup.org/.
2 Its website is http://www.vtk.org.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
6.4 Example Visualization Tasks 111
they tend to be very complicated, with again a steep learning curve. Because they are
not trivial to write, they are often commercial products with a very expensive price tag.
Fortunately, there are some top-quality examples in the public domain, and a far from
complete list includes (in alphabetical order) Mayavi,3 Paraview,4 both of which are
based on VTK, and Visit,5 which uses silo or HDF. All three offer a Python interface to
access their features. There is, however, a snag with the last two of the three examples
listed. Like Python, they are complicated programs which are still being developed.
Because of the complexity involved in compiling them, they all offer ready-to-run binary
versions for all of the major platforms. Naturally, the Python interfaces work only with
the Python version that was used when the binary program was created, and this may
not be the same version as that of the end user. However, because it is part of many
Python distribution packages, Mayavi should suffer less from this problem. It has the
major advantage that, when used with the mlab module, the user does not have to master
the intricacies of raw VTK code.
The philosophy of this book is both to try to minimize the addition of external software
to the Python base, and, more importantly, to eliminate steep learning curves. Python
avoids both of these disadvantages by offering several different approaches, which are
very similar to those we have already discussed. To illustrate their use we shall discuss
four different tasks. Note that, in all of these examples, the data is specified analyti-
cally a priori. This is so as to focus attention on the visualization. In real-life problems
the data comes from experiments or numerical approximations. However, the solutions
presented here are a starting point for the visualization of real-life problems.
One of the most studied non-linear wave equations is the Korteweg–de Vries equation
for the function u(t, x),
ut + u3 + 6uu x = 0,
where ut = ∂u/∂t etc. There is an exact solution, a solitary wave, given by
c
u(t, x, c) = √ ,
2 cosh ( c/2)(x − ct) )2
where c > 0 is a constant parameter.
We consider three “cut plane” tasks, which can share much of the same code. First
suppose we regard u as a function of x alone, but depending on two (constant) parame-
ters t and c. How does the graph change as we vary the choices for t and c? We shall call
3 Its website is http://mayavi.sourceforge.net.
4 It can be found at http://www.paraview.org.
5 It is at http://wci.llnl.gov/codes/visit.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
112 Multi-Dimensional Graphics
this the interactivity task. The next is the animation task; holding, say, c fixed, can we
make a quick animation of u(t, x) for a fixed range of x as t varies? It does not have to be
refined, but it does have to be quick and (almost) effortless. Finally, we have the movie
task, namely making a more sophisticated movie for presentation purposes, showing u
as a function of x as t varies, while c is held constant.
Investigation of these tasks reveals an unanticipated and unwelcome facet of Mat-
plotlib. This package, through its elaborate back-end structure (see Section 5.2.2), does
an exemplary job of bringing platform independence to two-dimensional graphics. How-
ever, attempts to maintain this independence when tackling more ambitious tasks via the
animation module have been less successful. The lack of docstrings at a pedagogic level
makes it very difficult for the casual user to use this module. Fortunately the IPython
notebook concept has effected a partial rescue. Whichever browser is used, the HTML
language is employed, and this offers both platform independence and new graphics
back-end features never envisaged by Matplotlib’s originators. The last two tasks exem-
plify different ways of using (or not using) these features.
10 fig, ax = plt.subplots()
11 plt.subplots_adjust(left=0.15, bottom=0.30)
12 plt.xlabel("x")
13 plt.ylabel("u")
14 x = np.linspace(-5.0, 20.0, 1001)
15 t0 = 5.0
16 c0 = 1.0
17 line, = plt.plot(x, solwave(x, t0, c0), lw=2, color=’blue’)
6 It is available from http://matplotlib.org/api/widgets_api.html.
7 They are near the bottom of http://matplotlib.org/examples/index.html.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
6.5 Visualization of Solitary Waves 113
20 axcolor = ’lightgoldenrodyellow’
21 axtime = plt.axes([0.20, 0.15, 0.65, 0.03], axisbg=axcolor)
22 axvely = plt.axes([0.20, 0.1, 0.65, 0.03], axisbg=axcolor)
23
27 def update(val):
28 time = stime.val
29 vely = svely.val
30 line.set_ydata(solwave(x, time, vely))
31 fig.canvas.draw_idle()
32
33 svely.on_changed(update)
34 stime.on_changed(update)
35
40 def reset(event):
41 svely.reset()
42 stime.reset()
43
44 button.on_clicked(reset)
45
46 plt.show()
Lines 6–8 define the waveform. Line 10 shows how to obtain both a Figure and an Axes
instance with one command. Line 11 adjusts the placing of the plot. Line 17 sets up
line as an identifier for the line drawn with the default parameters. plt.plot delivers
a tuple with one member, and this is why the comma must appear on the left-hand side.
You will need to study the docstrings to find out how the rest of the code works. It
should be easy enough to adapt for a different problem, more sliders etc.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
114 Multi-Dimensional Graphics
can be downloaded8 and installed according to the recipe in Section A.5. We show how
to produce a simple animation of the solitary wave with c = 1 from within the IPython
notebook.
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
from matplotlib import animation
from JSAnimation import IPython_display
def solwave(x,t,c=1):
""" Solitary wave solution of the K deV equation."""
return c/(2*np.cosh(np.sqrt(c)*(x-c*t)/2)**2)
1 # Initialization
2 fig = plt.figure()
3 ax = plt.axes(xlim=(-5, 20), ylim=(0, 0.6))
4 line, = ax.plot([], [], lw=2)
5
6 t=np.linspace(-10,25,91)
7 x = np.linspace(-5, 20.0, 101)
8
9 def init():
10 line.set_data([], [])
11 return line,
12
13 def animate(i):
14 y = solwave(x,t[i])
15 line.set_data(x,y)
16 return line,
17
In line 3 we fix the axis limits a priori, to avoid unnecessary jitter in the animation. The
only thing which will change is the line drawn, and we set this up as a tuple in line 4,
and initialize it in the function init() defined in lines 9–11. The work of producing
the ith frame is carried out in the function animate(i), lines 13–16, using the t and x
arrays defined in lines 6 and 7. Finally the animation is produced by line 18. Most of
the controls should be well-known. The extreme left (-) and right (+) buttons slow down
and speed up the animation respectively.
8 Its home page is https://github.com/jakevdp/JSAnimation.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
6.5 Visualization of Solitary Waves 115
1 import numpy as np
2
19 x=np.linspace(-5,20.0,101)
20 t=np.linspace(-10,25,701)
21
22 for i in range(len(t)):
23 file_name=’_temp%05d.png’ % i
24 plot_solwave(t[i],x)
25 plt.savefig(file_name)
26 plt.clf()
27
28 import os
29 os.system("rm _movie.mpg")
9 See http://www.ffmpeg.org.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
116 Multi-Dimensional Graphics
30 os.system("/opt/local/bin/ffmpeg -r 25 " +
31 " -i _temp%05d.png -b:v 1800 _movie.mpg")
32 os.system("rm _temp*.png")
The main loop, lines 22–26, now carries out the following steps for each i. Create a file
name made up of _temp, i and the suffix png. Then plot the frame and save it to file
using the name just generated. Finally, line 26, clear the frame for the next plot. At the
end of this step, we have very many files _temp00000.png, _temp00001.png, . . . We
could of course have chosen another file format rather than png, but this one is probably
a reasonable compromise between quality and size.
For the second part of the process, we import the os module, and in particular the
function os.system(). This is very simple. It accepts a string, interprets it as a com-
mand line instruction and carries it out. This snippet assumes a Unix/Linux operating
system, and Windows users will need to modify it. The first invocation, line 29, of
os.system deletes a file _movie.mpg (if it exists) from the current directory. As writ-
ten, the second occurrence creates a command line invocation for ffmpeg. Note that
Python does not know your user profile, so the first item in the string is the absolute
path name of my copy of the ffmpeg binary. Almost certainly yours will differ, and you
will need to edit this line. Then follow the parameters that ffmpeg needs. The basic ones
are -r 25, i.e., 25 frames per second, and -b:v 1800, the bit rate. The choices here
work for me. The input files follow -i, and _temp%05d.png is interpreted as the col-
lection of frame files we have just created. The last item is the output file _movie.mpg.
The final line deletes all of the frame files we created earlier, leaving the movie file,
which can be viewed using any standard utility. Of course, other choices of movie for-
mat rather than mpg are possible. Furthermore, you may replace ffmpeg by any utility
you prefer, by modifying lines 30–31.
This snippet creates and then destroys 701 frame files. Its speed depends on your in-
stallation. The interpreter may issue some size warnings, which you may safely ignore.
The reason for using Python to carry out these system commands should be evident.
Once we are happy with the code, we can wrap it as a self-contained function for mak-
ing movies! This is an instance of the use of Python as a scripting language.
We shall consider three cases, with a concrete example for each. Our examples are
artificial in the sense that they are predefined analytically. However, in setting them up
we have to construct finite sets of discrete data. In the real world, we would instead use
our own finite sets of discrete data, derived either from experiment or by a complicated
numerical simulation.
The first is a parametrized curve x(t) = (x(t), y(t), z(t)), and as a specific example we
consider the curve Cnm (a)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
6.6 Visualization of Three-Dimensional Objects 117
where t ∈ [0, 2π], n and m are integers and 0 < a < 1. This is a spiral wrapped round
a circular torus, with major and minor radii 1 and a respectively, a three-dimensional
generalization of Lissajous’ figures.
The second is a surface of the form z = z(x, y). Here we consider the artificial but
specific example
z = e−2x −y2
2
cos(2x) cos(3y) where − 2 x 2, −3 y 3.
For the third case, we shall treat the more general situation of a parametrized surface
x = x(u, v), y = y(u, v), z = z(u, v), which reduces to the case above if we choose
x = u, y = v. As a concrete example, we consider the self-intersecting minimal surface
discovered by Enneper
x = u(1 − u2 /3 + v2 ), y = v(1 − v2 /3 + u2 ), z = u2 − v2 where − 2 u, v 2.
It is not difficult to construct similar examples with a higher number of dimensions.
There is an issue which is almost trivial in two dimensions, namely the relationship,
if any, between the data values. In two dimensions, when we call ax.plot(x,y), x
and y are treated as linear lists, and a line is drawn between contiguous data points. If
we believe the data to be uncorrelated, then ax.scatter(x,y) would simply show the
data points.
In three dimensions, there is more freedom. For example, if we were to plot the sec-
ond example above when x and y were uniform arrays, e.g., as generated by np.mgrid,
we would be tessellating a two-dimensional surface by rectangles. The Enneper surface
above is a more general tiling by quadrilaterals. However, we might consider also a tes-
sellation by triangles, or even impose no structure at all and simply plot the points. In
higher dimensions, there are many more possibilities.
We shall discuss two different software approaches for each example. The first is
an mplot3d module for Matplotlib. This means that much of the experience gained in
Chapter 5 can be reused. Furthermore, the output will be high-definition vector graphics
as usual. There is, however, a downside. We are dealing with very many more points,
see the argument at the end of Section 6.2. Because of this, trying to implement the
interaction or movie tasks discussed in the last section will be horrendously slow.
Because of this slowness we consider also the Mayavi package, and in particular the
mlab module which can be loaded from Mayavi, which is itself a Python package. This
makes many of the features of Mayavi available with a Matplotlib-like interface. For
the documentation, see the user guide, Ramachandandran and Variquaux (2009). How-
ever, Mayavi is not without its problems. Development seems to have finished about
2010, when it was called Mayavi2, and this is reflected in the documentation. The mod-
ule depends on several non-Pythonic packages, and as a result it is quite difficult to
suggest a way of using it directly from the IPython interpreter, which is both platform-
independent and distribution-independent.10 There is, however, a simple work-around,
which we describe below.
In the next three sections, we look at the two approaches to visualization for each of
10 Enthought have sponsored the Mayavi project, and the Canopy implementation should work on most
platforms. It is, alas, currently not straightforward to implement Mayavi within Anaconda.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
118 Multi-Dimensional Graphics
our three examples. The fastest and most satisfactory way to master visualization is to
try out the code snippets and then experiment with them and with other examples. The
use of the IPython notebook mode is strongly recommended.
We now consider the curve Cmn (a) from the first example of Section 6.1.1. We shall
make the arbitrary parameter choices a = 0.3, m = 11 and n = 9. You might wish to
experiment with other choices.
The next snippet uses the first to visualize the curve using matplotlib package mplot3d.
It is being assumed that the highly recommend Jupyter notebook is being used here.
1 import numpy as np
2
3 theta=np.linspace(0,2*np.pi,401)
4 a=0.3 # specific but arbitrary choice of the parameters
5 m=11
6 n=9
7 x=(1+a*np.cos(n*theta))*np.cos(m*theta)
8 y=(1+a*np.cos(n*theta))*np.sin(m*theta)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
6.7 A Three-Dimensional Curve 119
9 z=a*np.sin(n*theta)
10
15 plt.ion()
16 fig=plt.figure()
17 ax=Axes3D(fig)
18 ax.plot(x,y,z,’g’,linewidth=4)
19 ax.set_zlim3d(-1.0,1.0)
20 ax.set_xlabel(’x’)
21 ax.set_ylabel(’y’)
22 ax.set_zlabel(’z’)
23 ax.set_title(’A spiral as a parametric curve’,
24 weight=’bold’,size=16)
25 #ax.elev, ax.azim = 60, -120
26 fig.savefig(’torus.pdf’)
Lines 1–12 should be familiar. Line 13 introduces the Axes3D class object which will be
needed for carrying out the visualization. Next, lines 16 and 17 join up the two concepts:
ax is an instance of the object Axes3D tied to an instance fig of the matplotlib Figure
class. The object-oriented style for using Matplotlib was introduced in Chapter 5 and is
the norm for mplot3d. Thus line 18 actually draws the curve, and lines 20–24 and 26
carry out operations familiar from the two-dimensional case.
The figure should be shown in the familiar Matplotlib window, where the interac-
tive buttons behave as before (see Section 5.2.4). There is important new functionality,
however. Simply moving the mouse over the figure with the left button depressed will
change the observer direction. A similar movement using the right button will zoom the
figure both in and out.
As written, the snippet will save the figure drawn with the default values of azimuth
and elevation, −60◦ and 30◦ respectively, which are rarely the most useful ones. By
experimenting, we can establish more desirable values. The commented-out line 25
shows how to set them to values that are more desirable for this particular image. If
you rerun the snippet with this line uncommented and amended, then the desired figure
will be saved, as in, e.g., Figure 6.1. Figures for presentations often look better if a
title is present, whereas they are usually unnecessary for books. As a compromise, both
Figures 6.1 and 6.2 carry titles, to show how to implement them, but they are omitted
for the rest of this chapter.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
120 Multi-Dimensional Graphics
Figure 6.2 An example of a curve wrapped round a torus using the Mayavi mlab module with
the default settings.
3 theta=np.linspace(0,2*np.pi,401)
4 a=0.3 # specific but arbitrary choice of the parameters
5 m=11
6 n=9
7 x=(1+a*np.cos(n*theta))*np.cos(m*theta)
8 y=(1+a*np.cos(n*theta))*np.sin(m*theta)
9 z=a*np.sin(n*theta)
10
The result is shown in Figure 6.2. Lines 1–10 are the same as for the mplot3d version.
Line 12 draws the curve. Here we have chosen to colour the curve according to the value
of sin(nθ), where θ is the curve’s parameter, using the colour map spectral. (Colour
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
6.8 A Simple Surface 121
maps are discussed below.) Lines 14 and 15 produce axes and a title. The docstrings
give further possibilities. (Note that the Mayavi developers have chosen the Matlab-
compatible version of names for functions to decorate figures. See Section 5.10 for a
discussion of this issue.) Do not attempt to run this snippet, but instead save it to mem-
ory as, say, torus.py. Next run the Mayavi application. From the File menu select Run
Python Script and choose the file torus.py. The application will draw the curve, as
seen along the z-axis. The window here is rather different from that of Matplotlib. Once
the figure has been drawn we can change the viewing angle by dragging the mouse
across it with the left button depressed. The right button offers pan and zoom facilities.
At the top left, there are 12 small buttons for interacting with the figure. By default, the
background colour is black, and button 12 (on the right) allows us to change this. But-
ton 11 is used for saving the current scene. The formats available are implementation-
dependent, but all implementations should allow the scene to be saved as a png file.
On the left is the Mayavi “pipeline” that mlab constructed to draw the figure. This
can be used to edit the figure in situ. For example, the title is far too small, and in the
wrong position. Click first on Title in the pipeline, and then on Mayavi object editor
below the pipeline. Next click on TextProperty and use the displayed menu to change
the font size, font style and position.
The reader is urged to experiment further with the other pipeline elements, and/or
peruse the Mayavi documentation.
In this section, we consider the simple surface defined above in Section 6.1.1 to be the
graph of
z = e−2x −y2
2
cos(2x) cos(3y) where − 2 x 2, −3 y 3.
9 plt.ion()
10 fig=plt.figure()
11 ax=Axes3D(fig)
12 ax.plot_surface(xx,yy,zz,rstride=4,cstride=3,color=’c’,alpha=0.9)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
122 Multi-Dimensional Graphics
Figure 6.3 An example of a simple surface with three contour plots using the
Matplotlib.mplot3d module.
13 ax.contour(xx,yy,zz,zdir=’x’,offset=-3.0,colors=’black’)
14 ax.contour(xx,yy,zz,zdir=’y’,offset=4.0,colors=’blue’)
15 ax.contour(xx,yy,zz,zdir=’z’,offset=-2.0)
16 ax.set_xlim3d(-3.0,2.0)
17 ax.set_ylim3d(-3.0,4.0)
18 ax.set_zlim3d(-2.0,1.0)
19 ax.set_xlabel(’x’)
20 ax.set_ylabel(’y’)
21 ax.set_zlabel(’z’)
22 fig.savefig(’surf1.pdf’)
Here lines 3 and 4 define the surface. Here lines 6–11 have appeared before in the snip-
pet used to visualize a parametric curve. Line 12 draws the surface. The parameters
rstride=4, cstride=3 mean that every fourth row and third column are actually
used to construct the plot. The parameter alpha=0.9 controls the opacity11 of the sur-
face, a floating-point number in the range [0, 1]. See the function’s docstring for further
potential parameters. Again we have seen lines 19–22 before.
If a “wire frame” representation of the surface is preferred, it can be obtained by
replacing line 15 by
ax.plot_wireframe(xx,yy,zz,rstride=4,cstride=3)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
6.8 A Simple Surface 123
Another new feature here is the drawing of contour plots. Recall that in Section 5.9
we showed how Matplotlib could visualize the level contours of a surface z = z(x, y).
In line 13 of the current snippet, we do the same after rearranging the functional rela-
tionship to be x = x(y, z). We have to position the plot as a yz-plane somewhere and the
parameter offset=-3 puts it in the plane x = −3. Now the default x-range was set by
snippet B to be x ∈ [−2, 2], which would render the contour plot invisible. Therefore,
line 16 resets the x-range to be x ∈ [−3, 2]. Lines 14, 15 and 17, 18 deal with the other
coordinate directions. Of course, there is no obligation to draw all three contour plots,
or even one or two of them. Your figures will certainly look less cluttered if you make a
more judicious choice of what plots, if any, to include.
Figure 6.4 An example of a simple surface visualized using Mayavi’s mlab module.
Figure 6.4 was drawn using the following code snippet, which should be saved to a file,
say surf2.py, before Mayavi is invoked.
1 import numpy as np
2
3 xx,yy=np.mgrid[-2:2:81j, -3:3:91j]
4 zz=np.exp(-2*xx**2-yy**2)*np.cos(2*xx)*np.cos(3*yy)
5
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
124 Multi-Dimensional Graphics
The new feature here is line 8, and perusal of the relevant docstring is worthwhile. Note
in particular that ’surface’ is the default, and replacing it with ’wireframe’ pro-
duces an alternative representation. Setting warp_scale to a float value will magnify
(or contract) the z-axis.
As for the curve, the orientation and scale of the surface are best controlled with the
cursor. Interaction with the pipeline elements is the easiest way to control the depiction
of the surface. Consult the documentation!
We turn now to the visualization of Enneper’s surface, which was defined in Section
6.1.1 by
Here the difference between the two plotting tools becomes significant. There has to be
a trade-off between precision and speed.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
6.9 A Parametrically Defined Surface 125
8 plt.ion()
9 fig=plt.figure()
10 ax=Axes3D(fig)
11 ax.plot_surface(x.T,y.T,z.T,rstride=2,cstride=2,color=’r’,
12 alpha=0.2,linewidth=0.5)
13 ax.elev, ax.azim = 50, -80
14 ax.set_xlabel(’x’)
15 ax.set_ylabel(’y’)
16 ax.set_zlabel(’z’)
17 #ax.set_title(’A parametric surface plot’,
18 # weight=’bold’,size=18)
The new features are all in line 11. Notice first that the underlying code is based on
image-processing conventions, so the Matplotlib commands expect two-dimensional
arrays to be supplied in image form. Thus we need to supply the transpose of the x-, y-,
and z-arrays.
It should be noted that this is a non-trivial self-intersecting surface, and its visualiza-
tion requires a judicious choice of the opacity, line width and stride parameters. Further,
the optimal choice may well depend on the viewing angle.
There is a great deal of computation going on here, which will tax even the fastest
processors. In particular, interactive panning and tilting will be slowed down signifi-
cantly.
Just as before we need to save this snippet as, say, surf4.py, and then run it from
within the Mayavi application. Interactive panning and tilting is noticeably faster than
using mplot3d. Why is this so? Well Matplotlib and mplot3d use “vector graphics”
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
126 Multi-Dimensional Graphics
which deal with shapes and are infinitely stretchable. Mayavi and mlab use “bitmapped
graphics” which create and store each pixel of the picture. The files are large but easy to
create. The big disadvantage is that only a limited amount of expansion or contraction
is possible without significant loss of visual quality. This why saving the figure works
satisfactorily only with a bitmap format such as png.
We turn now to the use of mlab to visualize figures that are more complicated. Our
example is only a slight alternative version of an earlier example, but the ambitious
reader should consult the mlab documentation to explore the exciting possibilities.
In Section 5.11, we sketched an introduction to fractal sets and showed how to con-
struct the classic visualization of the Mandelbrot set for the mapping z → z2 + c as
realized by the iteration (5.1). Here we shall be concerned with the Julia set for the
same mapping. That means that now we hold the parameter c fixed and let the iterates
zn depend on the initial value z0 . Then the escape parameter depends on z0 rather than
c, = (z0 ). Instead of the high-resolution, but fixed, image of Section 5.11, we look for
an interactive, but lower-resolution, picture, a so-called canyon view, as exemplified in
the following self-contained snippet, which is adapted from the Mayavi documentation,
Ramachandandran and Variquaux (2009), and is not optimized for speed. Its purpose is
to alert the reader to other visual possibilities.
1 import numpy as np
2
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
6.10 Three-Dimensional Visualization of a Julia Set 127
Lines 1–7 are straightforward. The grid julia is set initially to zeros, and an arbitrary
choice for c is made. In lines 10–13, we perform 100 iteration steps. escape is a grid
of Booleans which are False if the corresponding |zn | ≤ 2 and are True otherwise. For
the escaped points, we increment julia by a small amount. Finally, lines 16–19 draw
the figure. The parameters in lines 17–19 were chosen by trial and error. The output of
this snippet is shown in Figure 6.7.
The reader is urged to try out this code, and then to make changes. The most passive
way to do this is simply to change the code and then rerun it. More versatility can be
obtained by using the cursor to modify the picture and button 12 to change values. Left
clicking on any pipeline item will reveal the options chosen and offer the means to
change them. With a little practice it’s actually very easy and adaptable.
Obviously, in an introductory text we cannot survey all of the possibilities for three-
dimensional graphics. In particular, we have not covered the important topic of anima-
tions. The reader is therefore urged to explore further the abilities of the mlab module,
using the recommended documentation. Once the reader has mastered the basics, it is
very easy to produce sophisticated graphics and movies, which are not readily achiev-
able in mplot3d. The price for this versatility is that Mayavi’s mlab cannot match the
extremely high resolution of Matplotlib’s matplotlib.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
128 Multi-Dimensional Graphics
Figure 6.7 A Julia set for the mapping z → z2 + c with c = −0.7 − 0.4i. Points near the top of the
“canyon” correspond to trajectories which escaped to infinity early, while those at the bottom
have yet to escape.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.008
7 SymPy : A Computer Algebra
System
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
130 SymPy : A Computer Algebra System
graphics. An invalid criticism is that there is little active maintenance; this is fifty-year-
old software, and any remaining bugs are very discreet. A very major drawback is that
there is virtually no up-to-date third-party documentation, tutorials etc.
Maxima has also become open source. The basic console version can be downloaded
for all major platforms.2 However most users prefer to use the graphical interface ver-
sion wxMaxima.3 The manual, at 1144 pages, is somewhat verbose, although it is well
laid out. Much of it is available online when required in the wxMaxima interface. Many
of the sections are state of the art, but some are broken, and most unlikely to be repaired
any time soon. Third party documentation is widespread, but not always up-to-date.
The third open-source CAS is SageMath.4 This project aims to bring together the best
of open-source mathematical software in one, truly vast, package. The “glue” which
holds it all together is, of course, Python. The CAS section actually includes the best
features from both SymPy and Maxima. The “notebook” mode is very similar to the
Jupyter notebook, offering a more than adequate graphical user interface. Documenta-
tion, including third-party offerings, is excellent. Relativists should note that there is a
superb SageManifolds extension,5 which is well worth perusing.
SageMath is probably the best modern tool for serious projects, but SymPy is an
excellent introduction to it, and, for many users, the comparatively lightweight features
may be all that they need. In the rest of this chapter we present a review of SymPy’s
main features, assuming IPython is running in notebook mode. If you see a function
which looks useful for you then do inspect the docstring: it’s so easy.
Many of the examples in the documentation start with the line
from sympy import *
but, as we pointed out in Section 3.4, this is a dangerous procedure. Instead we have
introduced a more or less arbitrary abbreviation, and use it when calling SymPy func-
tions.
import sympy as sy
sy.init_printing()
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
7.2 Symbols and Functions 131
This will not work in SymPy, principally because SymPy is a Python library and so its
syntax must conform to that of Python. The snippet fails, first because the identifier x is
not defined. We have to associate x with a definite value, e.g., x = 4, and this defeats the
purpose: we want to leave x and y to take unknown arbitrary values. SymPy gets around
this by defining a new entity, a symbol, actually a Python class, see Section 3.9. There
are several ways to create class instances, and perhaps the most common way to handle
the snippet above is to insert the line x,y = sy.symbols("x y"), see below. Python
will still complain because exp(x) is undefined. It recognizes a function of course,
but has no definition for the exponential of a symbol. SymPy of course contains such
definitions. Do try out the following snippet.
1 import sympy as sy
2 sy.init_printing()
3 x,y = sy.symbols("x y")
4 D = (x+y)*sy.exp(x)*sy.cos(y)
5 D
Notice that SymPy neither knows nor cares about the number of function arguments. It
is the user’s responsibility to check consistency of argument values.
Especially when considering simplification, it is helpful to know that certain symbols
always contain special values. For a real-life example consider the equation, ubiquitous
in complex variable courses, z = x + iy. It is tacitly assumed that both x and y take real
values only. Otherwise the splitting into real and imaginary parts does not make sense.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
132 SymPy : A Computer Algebra System
Within SymPy, suppose that i and j take integer values, while u and v are always real.
This is indicated by
The last line reminds one that symbols really are class instances; i.e., they have at-
tributes, see Section 3.9. You might like to use introspection, see Section 2.2, to inves-
tigate what other attributes are available for u. √
As can be seen from the output of this snippet, neither i nor j represents −1 in
SymPy. It is catered for as sy.I. Also very useful are e, π and ∞, available as sy.E,
sy.pi and sy.oo respectively.
As we have said, expressions are immutable. So how does one find out the result of
substituting specific values for, say, x and y in D? This is catered for by the substitution
operator, which is available either as a function sy.subs(), or more commonly, as a
class method. For example, to find D when x = 0,
D0 = D.subs(x,0)
D0pi = D0.subs(y, sy.pi)
D0, D0pi, D.subs([(x,0), (y, sy.pi)])
where the third element shows how to make many substitutions at once using a list of
(old, new) tuples. We repeat: symbols such as D are immutable, so D is not changed
by the substitution operation in the first line.
Suppose x and y are symbols. So are x+1 and 0.5*y. The integer and float are widened
to symbols. Fractions need a little care. Consider the snippet
x + 1/3
Python parses “1/3” as integer division and returns 0, which is probably not what was
intended. Examine next the output from the snippet
which shows one way of coping with the vagaries of integer division. Although the
SymPy function sy.Rational is extremely useful, there are other more user-friendly
ways to achieve the same effect.
The function sy.S converts singleton expressions, given as strings, to their SymPy
equivalents. Compare the snippet above with
x + sy.S(’1/3’), sy.S("1/2")*y
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
7.4 Matrices and Vectors 133
D_s = sy.sympify(’(x+y)*exp(x)*cos(y)’)
cosdiff = sy.sympify("cos(x)*cos(y) + sin(x)*sin(y)")
D_s, cosdiff
D_s should behave in an identical manner to the D defined earlier. cosdiff will be used
below. The docstrings for py.S and sympify are extremely informative.
The expression cosdiff actually represents cos(x − y). If we need only a few explicit
values the function subs, mentioned above, will provide their symbolic values. We
can obtain numerical values using the function sy.evalf, which takes a single integer
argument, the precision. Much of the functionality of sy.evalf is also available in
sy.N, which is, very nearly, a duplicate. Consider the following example.
cospi4 = cosdiff.subs([(x,sy.pi/2),(y,sy.pi/4)])
cospi4, cospi4.evalf(), cospi4.evalf(6), sy.N(cospi4, 6)
All of this is perfectly adequate for a few selected values of the arguments, but what
if we want to use the expression as a function, preferably a ufunc in the sense of Section
4.1.4? The SymPy function sy.lambdify does precisely that. It takes as arguments
a tuple of the function arguments, the name of the expression and the Python base
library to be used. In practice it’s extremely easy! As usual, perusal of the docstrings is
extremely enlightening.
import numpy as np
func = sy.lambdify((x, y), cosdiff, ‘‘numpy’’)
xn = np.linspace(0, np.pi,13)
func(xn, 0.0) # should return np.cos(xn)
For the sake of completeness we introduce matrices and vectors at this point. SymPy
implements matrices with the Matrix class, and an n-vector as an n×1 matrix. The con-
structor requires an ordered list of rows, where each row is an ordered list of elements.
A very simple example is given below. As one can see, multiplication here means matrix
multiplication
M = sy.Matrix([[1,x],[y,1]])
V = sy.Matrix([[u],[v]])
M, V, M*V
The command M.tab will reveal a surprisingly large number of functions/methods which
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
134 SymPy : A Computer Algebra System
can be applied to matrices, and, if one of them is of interest to you, the corresponding
docstring should be studied.
A useful one is eigenvects, which returns a list of tuples, where each tuple contains
an eigenvalue, a multiplicity and a basis for the eigenvectors.
M.eigenvects()
For square matrices, the transpose, determinant and inverse are readily available as at-
tributes. We will discuss shortly the simplification of the last two expressions below.
M.T, M.det(), M.inv(), M*M.inv()
We stated above that symbols were immutable. This assertion needs to be qualified,
and at this point the beginner might like to review the same issue for lists, see Section
3.5.4. A matrix M is immutable, but its contents may be changed freely, see the following
snippet.
M[0,1]= u
M
7.5.1 Differentiation
Suppose D is defined as above. There are two ways to compute first-order derivatives.
sy.diff(D,x), D.diff(x)
The second version relies on the fact that D is a symbol, i.e., a class, and this class
contains the derivative function. Higher derivatives are computed in what should be
obvious ways, e.g.,
D.diff(x,y,y), D.diff(y, 2, x), f(x, y).diff(y, 2, x)
Sometimes we may not wish to compute the derivative explicitly, in so-called “lazy”
differentiation. This is accomplished by the sy.Derivative function; note the capital
letter. If, later, we want to carry out the evaluation, then the class method doit carries
out the postponed calculation.
D_xyy = sy.Derivative(D,x,y,2)
D_xyy, D_xyy.doit()
7.5.2 Integration
Indefinite integration is the inverse of differentiation and SymPy treats it accordingly.
As with all computer algebra systems, the “constant of integration” is never shown
explicitly. The following snippet treats some straightforward cases.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
7.5 Some Elementary Calculus 135
Definite integration is very similar. One simply replaces the variable of integration pa-
rameter by the tuple (variable, lower limit, upper limit). For example,
sy.integrate(D, (y,0,sy.pi)), D.integrate((y,0,sy.pi))
Multiple definite integrals are handled in the obvious way. For example,
sy.integrate(sy.exp(-x**2-y**2), (x, 0, sy.oo), (y,0,sy.oo))
If SymPy cannot integrate your expression, it will eventually, after trying all of its store
of algorithms, return the “lazy” integral form, a mark of failure. This can be deceptive.
If you try to compute
x
dx,
sin x
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
136 SymPy : A Computer Algebra System
then SymPy cops out with the “lazy” form. But SageMath can handle it. However, if you
try to compute
esin x dx,
then, currently, no CAS can handle it, and it is doubtful whether a closed form is possi-
ble.
We could consider first expanding foo as a Taylor series in x. This ought to be uniformly
convergent, because the exponential series for ey is, for arbitrary y. Then term-by-term
integration should give a useful approximation. SymPy allows us to calculate, e.g., the
first 10 terms in the Taylor series expansion.
foo_ser = foo.series(x, 0, 10)
foo_ser, foo_ser.integrate(x)
If series expansions are useful for you, then you need to consult its docstring, which
will probably tell you to look at the docstring for the sy.Expr.series function. The
remainder term obeys the usual applied mathematicians’ “big-Oh” rules. If it bothers
you, then remove it via a class attribute, e.g.,
foo_ser.removeO()
At points of discontinuity, limits from below and from above will differ. By default
sy.limit takes the limit from above, but both limits are readily available.
goo = 1/(x - 1)
goo, sy.limit(goo, x, 1)
As our snapshot of some of SymPy’s capabilities has shown, it can create quite compli-
cated expressions, which might not be useful, immediately, to the user. Can we simplify
them? How would we test two long expressions to see whether they are the same?
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
7.6 Equality, Symbolic Equality and Simplification 137
Let us start from pure Python. As we have seen, the “equals” sign (=) has nothing to
do with equality; it is the Python assignment operator (e.g., a=3), and SymPy of course
respects this. Numerical equality is handled by the double equality sign ==, e.g.,
12/3 == 4
Unfortunately for the end-user, the SymPy developers have reserved == for “Symbolic
equality”, which is not what the user wants. As an example, consider the two expres-
sions
ex1 = (x+y)**2; ex2 = x**2 + 2*x*y + y**2
ex1 == ex2
They are not “symbolically equal”, for one is a product of sums, while the other is a
sum of products. Even their difference is not “symbolically” zero.
ex1 - ex2, ex1 - ex2 == 0
The functions sy.expand and sy.factor do very much what one might expect, and
for further examples one should examine the docstrings. The docstring for sy.expand
is quite complicated, for this is an extraordinarily versatile function. In particular there
are many ways to extend its scope to more specialized expressions. For example,
sy.expand_trig(sy.sin(x+y)), sy.expand(sy.cos(x-y),trig=True)
Many other versions are available. Try sy.expand_tab for a partial list.
One has to be particularly careful when treating powers, because a number of ap-
parent√identities are not true. For instance, it is not true that (xa )b ≡ xab in general,
e.g., x2 x: just consider ((−1)2 )1/2 = 1 (−1)2×1/2 = (−1)1 = −1. However,
the identity is true if b is an integer. Similarly, it is not true that xa ya ≡ (xy)a : try
x = y = −1, a = 1/2. This identity does hold if both x and y are positive real numbers
and a is real.
SymPy is well aware of these problems and will not use these identities unless the
user set up the symbols with the appropriate restrictions in py.symbols.
An extremely useful function is sy.cancel, which will take any rational expression
and try to reduce it to canonical form. As an example, consider the matrix M defined
and altered above.
A = M*M.inv(); A
Now apply
A[1,0] = A[1,0].cancel(); A[1,1] = A[1,1].cancel(); A
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
138 SymPy : A Computer Algebra System
To see why sy.cancel is not a default option, try out this simple example!
c = (x**256 - 1)/(x-1)
c, c.cancel()
Readers of the last section could argue, with some justification, that there appears to be
no way of defining an “equation”, since = and == have both been used up. Traditionally
an equation has both a left-hand side lhs and a right-hand side rhs, e.g., using definitions
from above, let us set
lhs = D; rhs = cosdiff
There are two ways in SymPy to create an equation, both given in the following snippet.
eqn1 = sy.Eq(lhs, rhs); eqn2 = lhs - rhs
eqn1, eqn2
The expression eqn1 produces the traditional form of an equation. The second, eqn2,
is equally valid in contexts where an equation is expected. SymPy implicitly adds the
missing = 0.
From version 1.0, the developers of SymPy are changing the behaviour of solvers,
and so here we follow their latest recommendations. The universal workhorse is solve,
but, for equations in one variable, solvset is recommended, while one is asked to use
linsolve for linear systems of equations. Since these last two function names are most
unlikely to be overwritten, we import them directly.
from sympy.solvers import solveset
from sympy.solvers.solveset import linsolve
As the name suggests, solveset returns a set of solutions, so multiplicities are ignored.
However, sy.roots can supply these.
quad = x**2 - 2*x +1
solveset(quad, x), sy.roots(quad)
Simple transcendental equations are solved over the complex plane, by default.
solveset(sy.exp(x)-1, x)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
7.7 Solving Equations 139
solveset(sy.cos(x) - x, x)
The output is mathematics jargon for “x is a complex number which is also a member
of the set of numbers satisfying cos x = x”!
A x = b,
for an n-vector of unknowns x. The general case is also the simplest. This is when A
is non-singular, i.e., det A 0. Then the inverse A−1 exists, and so there is a unique
solution
x = A−1 b.
Next suppose A is singular. Consider first the set of vectors k such that Ak = 0. This is
called the kernel of A. It is easy to see that the kernel is actually a vector space. (Note
that in the previous case, where A was non-singular, the kernel is an empty set.) There is
another vector space associated with A, its range, which is the set of vectors r for which
one can find an x such that Ax = r. As an aside, there is an important result that the
sum of the dimensions of the kernel and range is n. We can now split the case that A is
singular into two subcases. Suppose first that the given vector b lies in the range. Then,
by definition we can find an x0 such that Ax0 = b. However consider x = x0 + k, where
k is an arbitrary vector in the kernel. Then Ax = A(x0 + k) = Ax0 + Ak = b + 0 = b.
Hence we have solutions, but infinitely many of them! However, if b is not in the range,
then there are no solutions at all: the equations are inconsistent.
Let us consider some concrete examples in two dimensions. Suppose
1 2 0
A= , b= ,
3 4 2
x + 2y = 0, 3x + 4y = 2.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
140 SymPy : A Computer Algebra System
We see that the range of A is the set of vectors (X, Y)T (i.e. the transpose of (X, Y)) such
that Y = 2X, while the kernel is the set of vectors (x, y)T such that x = −2y. Here both
are one-dimensional. Suppose first that b = (1, 2)T , i.e., we are considering the linear
system
x + 2y = 1, 2x + 4y = 2.
x = 1 − 2λ, y = λ, λ arbitrary.
It should be evident that the λ-contribution comes from the kernel. Finally consider the
case b = (1, 1)T , which is not in the range. The linear system
x + 2y = 1, 2x + 4y = 1
has no solutions!
We now examine how SymPy’s linsolve handles these three possibilities. Obvi-
ously this is a gross overkill for such simple systems, but the examples generalize in the
obvious way to much more complicated problems.
One approach is to specify the equations in scalar form within a list. For an explicit
example we consider the non-singular case. We also need to specify a list of the un-
knowns. Thus
Eqns = [x+2*y, 3*x+4*y-2]
linsolve(Eqns, [x,y])
In many cases it will be more convenient to specify the system matrix A and the
right-hand-side vector b. We consider the many-valued solution case.
A = sy.Matrix([[1,2],[2,4]])
b = sy.Matrix([[1],[2]])
linsolve((A,b),[x,y])
Here the solution is to be interpreted as follows. y can take arbitrary values, and then
x = 1 − 2y is determined.
If the equations are the output from some other function, they are frequently supplied
in augmented matrix form, i.e., the b-vector is adjoined as an extra last column to the
A-matrix. We illustrate this example with the final example discussed above.
A_b=sy.Matrix([[1,2,1],[2,4,1]])
linsolve(A_b,[x,y])
Here ∅ in the output is the mathematician’s standard symbol for the empty set, i.e., no
solutions.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
7.7 Solving Equations 141
The reader is directed to the docstring (linsolve?) for further details. However,
linsolve has an undocumented and undesirable feature. Suppose we consider the non-
linear system
y2 = (x + 1)2 , 3x − y = 1.
By taking the square root of the first equation, which involves a sign ambiguity, we have
two linear systems, with solutions x = 1, y = 2, and x = 0, y = −1. However, if we
submit this system to linsolve in the form
neq = [y**2 - x**2 - 2*x -1, 3*x - y -1]
linsolve(neq, [x,y])
This is the correct result. Next consider some equations involving square roots.
sys.solve([sy.sqrt(x) - sy.sqrt(y) -1, sy.sqrt(x+y) - 2], [x ,y])
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
142 SymPy : A Computer Algebra System
We now give some examples of what dsolve can do and what it cannot do. Let us start
with a single linear equation with constant coefficients.
ode1 = f(x).diff(x, 2) + 4*f(x)
sol1 = dsolve(ode1, f(x))
ode1, sol1
As usual by default, “ = 0” is missing from the differential equation. The correct general
solution is given, which includes two integration constants C1, C2, displayed as C1 , C2 .
Suppose we would like to impose initial conditions, say f (0) = 2, f (0) = 0. The
docstring suggests that there is a keyword parameter ’ics’ to do this, but at present it
does not work. Instead one has to work as follows. First we need to ensure that C1, C2
are declared as symbols. Then we carry out the relevant substitutions.
C1, C2 = sy.symbols("C1, C2")
fun = sol1.rhs
fund = fun.diff(x)
fun.subs(x,0), fund.subs(x,0)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
7.8 Solving Ordinary Differential Equations 143
Those first-order nonlinear equations which are “exact” are solved quickly.
ode2 = sy.sin(f(x)) + (x*sy.cos(f(x)) + f(x))*f(x).diff(x)
ode2, dsolve(ode2, f(x))
First-order equations with quadratic terms are often of Bernoulli type and usually
yield an exact solution.
ode3 = x*f(x).diff(x) + f(x) - sy.log(x)*f(x)**2
ode3, dsolve(ode3)
Notice that, if there is only one dependent variable, as here, it is not necessary to specify
it as an argument to dsolve.
Certain linear equations with variable coefficients are also easy to solve, e.g., an in-
homogeneous Euler equation.
ode4 = f(x).diff(x, 2)*x**2 - 4*f(x).diff(x)*x + 6*f(x) - x**3
ode4, dsolve(ode4)
It is not difficult to find simple soluble equations of third and higher orders.
Besides equations in one variable, we can also solve equations with two or more
variables. Now, according to the docstring dsolve needs a list of equations and a list of
unknowns. However, giving a list of unknowns produces an error, but omitting it works
as expected! Here is a simple constant-coefficient example.
ode7 = [f(x).diff(x) - 2*f(x) - g(x), g(x).diff(x) -f(x) - 2*g(x)]
ode7, dsolve(ode7)
Besides the infelicities we have pointed out above, there is a serious limitation for
elementary solvers such as dsolve. In many real-world examples the given equation is
not of a standard form, but is related to one via a simple transformation. Alas dsolve
cannot deal with this. A simple example suffices to illustrate the problem, viz., f (x) =
(x + f )2 .
ode8 = f(x).diff(x) - (x + f(x))**2
ode8, dsolve(ode8)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
144 SymPy : A Computer Algebra System
dsolve returns as a default the power series expansion about x = 0. However, consider
the simple transformation g(x) = x + f (x). This transforms the differential equation into
one with the independent variable absent, g (x) = 1 + g2 .
ode9 = g(x).diff(x) -1 - (g(x))**2
dsolve(ode9)
Thus the solution of ode8 is f (x) = tan(x − C1 ) − x, which dsolve has failed to pick
up.
Fortunately there is a large collection of ordinary differential equations and their so-
lutions.6 Perusal of this, and the monographs it cites, can, in this author’s experience,
be extremely productive.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
7.9 Plotting from within SymPy 145
purpose. It produces fully functional graphics functions in two and three dimensions,
including some features not available in Matplotlib. As always, the reader interested in
a particular function is urged to read the relevant docstring! (With IPython or the Jupyter
notebook, enter the command function_identifier?.)
We need of course to enable Matplotlib, see Chapter 5, and to import the plotting
module. Assuming a Jupyter notebook, the following snippet does the job.
%matplotlib notebook
import sympy.plotting as syp
The SymPy version of Matplotlib’s plt.plot function is called syp.plot, but the
syntax is rather different, and the docstring should be consulted for the various embel-
lishments.
Notice, in the snippet below, that we never have to specify the number of evaluation
points. The SymPy functions can determine what they think is needed. Of course, if
they get it wrong, there is always a manual override.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
146 SymPy : A Computer Algebra System
The output is shown in Figure 7.2. Because Matplotlib is in control, by giving the plot
object a name, here fig2, we can save it to a file, say foo.pdf, via an extra line, using
a class attribute.
fig2.save(’foo.pdf’)
SymPy plotting contains a functionality not present in Matplotlib, the ability to plot
one or more curves defined implicitly in the plane. Because syp.plot_implicit is a
new concept, we give a few examples of its use. The curve described by x2 + xy + y2 = 1
is an ellipse, but it is not so easy to give it in explicit or parametric form. The input to
syp.plot_implicit is the equation, and the ranges of the independent variables. One
can add various keyword parameters for decorattion, as shown as Figure 7.3.
syp.plot_implicit(x**2+x*y+y**2 - 1, (x,-1.5,1.5), (y,-1.5,1.5))
We now give an example with more complicated data. We are aiming to determine
the curve in the complex plane given by | cos(z2 )| = 1, where z = x + iy, and x and y are
real. We first redefine x and y within SymPy, ensuring that they take only real values.
We then define z = x + iy and w = cos(z2 ). We need to expand it in the form w = X + iY
for suitable X and Y, and that is what the next line does. Next we form wa = |w| with
the same expansion rules. (The reader may wish to review the relevant docstrings at this
point.) Finally drawing it is straightforward.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
7.9 Plotting from within SymPy 147
Figure 7.4 The implicitly defined curve | cos((x + iy)2 )| = 1 using SymPy’s plotting module.
The SymPy plotting functions know a lot about curves and surfaces in three dimen-
sions. We start with curves defined parametrically, and illustrate the function with a
single curve, a conical spiral. As always one should consult the docstring for further
possibilities.
syp.plot3d_parametric_line(u*sy.cos(4*u), u*sy.sin(4*u),
u, (u, 0, 10))
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
148 SymPy : A Computer Algebra System
Figure 7.5 The region in the first quadrant defined by x2 + y2 < 4 and xy > 1, determined using
SymPy’s plotting module.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
7.9 Plotting from within SymPy 149
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.009
8 Ordinary Differential Equations
Consider first a single second-order differential equation, e.g., the van der Pol equation
ÿ − μ(1 − y2 )ẏ + y = 0, t t0 , (8.1)
for a function y(t), with initial data
y(t0 ) = y0 , ẏ(t0 ) = v0 . (8.2)
Here μ, t0 , y0 and v0 are constants, ẏ = dy/dt, ÿ = d2 y/dt2 . We can rewrite this in a
standard first-order form as follows. Let
y y y[1]
y= , y0 = 0 , f(y) = . (8.3)
ẏ v0 μ(1 − y[0]2 )y[1] − y[0]
Then (8.1) and (8.2) combine to the standard form
ẏ(t) = f(y(t), t), t t0 , y(t0 ) = y0 . (8.4)
Because we specify sufficient conditions at an initial time t = t0 to fix the solution, this
is called an initial value problem. A very wide range of problems can be written in the
standard form (8.4), where y, y0 and f are s-vectors for some finite s.
A great deal of research has been devoted to the initial value problem (8.4). The
classic text is Coddington and Levinson (1955). Useful reviews with an emphasis on
numerical methods include Ascher et al. (1998), Butcher (2008) and Lambert (1992).
On the basis of this body of work, Python offers, via the add-on module SciPy, a
“black box” package for the solution of the initial value problem (8.4). Black boxes
offer convenience but also dangers. It is essential to understand something of how the
black box works and hence what its limitations are, and how to influence its behaviour.
Therefore, the next section sketches the basic ideas for the numerical integration of
initial value problems.
Here for simplicity we shall consider a single first-order equation for a function y(t),
ẏ = λ(y − e−t ) − e−t , t 0, y(0) = y0 , (8.5)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.2 Basic Concepts 151
Figure 8.1 The exact solution of the model problem (8.5) with y0 = 0, for various negative
values of the parameter λ.
tn = n/N, n = 0, 1, 2, . . . , N.
We have chosen an equidistant grid for simplicity, but in practice this is not essential.
Let yn = y(tn ). Thus y(t) is represented on this grid by the sequence {y0 , y1 , y2 , . . . , yN }.
Let the corresponding numerical approximation be denoted {Y0 , Y1 , Y2 , . . . , YN }.
Perhaps the simplest approximation scheme is the forward Euler one:
which corresponds to retaining only the first two terms in a Taylor series. Indeed, the
single-step error or local truncation error is
yn+1 − yn
τn = − (λyn − (λ + 1)e−tn ) = 12 hÿ(tn ) + O(h2 ). (8.8)
h
By choosing h sufficiently small (N sufficiently large), we can make the single-step error
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
152 Ordinary Differential Equations
|1 + hλ| 1 (8.11)
or
Yn+1 = (1 − hλ)−1 [Yn − h(λ + 1)e−tn ], n = 0, 1, 2, . . . , N − 1. (8.13)
1 There are a number of different stability criteria in the literature. This one corresponds to 0-stability.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.3 The odeint Function 153
We can repeat the earlier analysis to show that, with the obvious modification to τn in
equation (8.8), the single-step error τn = O(h) again, and the actual error satisfies
En+1 = (1 − hλ)−1 (En − hτn ),
with solution
n−1
En = (1 − hλ)−n E0 − h (1 − hλ)m−n τm .
m=0
It is quite difficult to write an effective efficient black box solver for the initial value
problem, but a few well-tried and trusted examples exist in the literature. One of the
best-known ones is the Fortran code lsoda, an integrator developed at Lawrence Liver-
more National Laboratory, as part of the odepack package. This switches automatically
between stiff and non-stiff integration routines, depending on the characteristics of the
solution, and does adaptive time-stepping to achieve a desired level of solution accuracy.
The odeint function in the scipy.integrate module is a Python wrapper around the
Fortran code for lsoda. Before we discuss how to use it, we need to understand a few of
the implementation details.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
154 Ordinary Differential Equations
y=odeint(f,y0,tvals)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.3 The odeint Function 155
for jac. The function odeint needs to be told this as a keyword argument specifying
a tuple, e.g., args=(alpha,beta). Finally, while developing experience with a new
package it is very helpful to be able to find out what went on inside the package. The
command
y,info=odeint(f,y0,tvals,full_output=True)
will generate a dictionary info giving a great deal of output information. See the doc-
string for details.
and suppose we want the solution at t = 1. To generate this we use the following code:
import numpy as np
from scipy.integrate import odeint
odeint(lambda y,t:y,1,[0,1])
to be solved and plotted for ω = 2 and 0 t 2π. We first rewrite the equation as a
system
y = (y, ẏ)T , ÿ(t) = (ẏ, −ω2 y)T .
The following complete code snippet will produce partially decorated pictures of the
solution in the notebook. (In real-life problems, we might want to decorate the figure(s)
more elaborately, following the methods in Chapter 5.)
1 import numpy as np
2 import matplotlib.pyplot as plt
3 from scipy.integrate import odeint
4 %matplotlib notebook
5
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
156 Ordinary Differential Equations
16 fig=plt.figure()
17 ax1=fig.add_subplot(121)
18 ax1.plot(t_arr, y, t_arr, ydot)
19 ax1.set_xlabel(’t’)
20 ax1.set_ylabel(’y and ydot’)
21 ax2=fig.add_subplot(122)
22 ax2.plot(y, ydot)
23 ax2.set_xlabel(’y’)
24 ax2.set_ylabel(’ydot’)
25 plt.suptitle("Solution curve when omega = %5g" % omega)
26 fig.tight_layout()
27 fig.subplots_adjust(top=0.90)
Four points should be noted here. The first is that in line 7, within the function rhs,
we have “unwrapped” the vector argument for the sake of clarity in the return values.
Secondly, in line 8 we have returned a tuple which will silently be converted to an array.
Thirdly, see line 13, in odeint the argument args must be a tuple, even if there is only
one value. The reason for this is explained in Section 3.5.5. Finally, the output is packed
as a two-dimensional array, and line 14 unpacks the solution arrays.
When, as here, the motion is autonomous, i.e., there is no explicit t-dependence, it
is often illuminating to produce a phase plane portrait, plotting ydot against y and
temporarily ignoring the t-dependence. We can use the methods of Section 5.10.2 to
achieve this in lines 21–24. Lines 25–26 were covered there. The reader might like to
peruse the relevant docstring for line 27.
However, with a little sophistication we can do a great deal more. First, we can create
a grid in phase space and draw direction fields, the vector ẏ(t) at each point of the grid.
Secondly, we can draw the solution curve starting from an arbitrary point of the figure,
i.e., arbitrary initial conditions.
Both steps are covered by the following snippet. Note that this has to be run as a file,
say traj.py, from terminal mode. Also, depending on your backend, you may need
several mouse clicks to get started.
1 import numpy as np
2 import matplotlib.pyplot as plt
3 from scipy.integrate import odeint
4
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.3 The odeint Function 157
6 y,ydot=Y
7 return ydot, -omega**2*y
8
13 fig=plt.figure()
14 y,ydot=np.mgrid[-3:3:21j, -6:6:21j]
15 u,v=rhs(np.array([y, ydot]), 0.0, omega)
16 mag=np.hypot(u, v)
17 mag[mag==0]=1.0
18 fig.quiver(y, ydot, u/mag, v/mag, color=’red’)
19
In line 14, we choose (y, ydot) coordinates to cover a relatively coarse grid in phase
space. (Note that the arrays y and ydot created in line 13 of the original snippet are
now lost.) In line 15, we compute the components (u, v) of the tangent vector field at
each point of this grid. A line like plt.quiver(y,ydot,u,v) will then draw these
values as little arrows whose magnitude and direction are those of the vector field and
whose base is the grid point. However, near an equilibrium point (u = v = 0) the arrows
end
√ up as uninformative dots. In order to alleviate this, line 16 computes the magnitude
u2 + v2 of each vector, and line 18 plots the normalized unit vector field. Line 17
ensures that no “division by zero” takes place. The docstring for plt.quiver offers
plenty of scope for further decoration.
The second part of the snippet draws trajectories with arbitrary initial conditions on
this phase plane. Line 23 shows the simple use of a very versatile function. The ginput
function waits for n mouse clicks, here the default n = 1, and returns the coordinates of
each point in the array choice. We then enter a while loop in line 24, and set y01 to
be the initial data corresponding to the clicked point in line 25. Now line 26 computes
the solution, and line 27 plots it. The program then waits, line 28, for further mouse
input. Thus you need to indicate two starting points to get the first trajectory drawn.
The default “timeout” for ginput is 30 seconds, so, if no input is given, line 29 will be
reached eventually. Only then is the final trajectory visible.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
158 Ordinary Differential Equations
The rapid relaxation to a periodic orbit suggests that this example could well become
stiff. Recalling the right-hand-side vector from (8.3), we compute the Jacobian matrix
0 1
J(y) = . (8.16)
−2μy[0]y[1] − 1 μ(1 − y[0]2 )
We set up next a Python script to integrate this equation, which extends the one used
earlier to integrate simple harmonic motion.
1 import numpy as np
2 from scipy.integrate import odeint
3
4 def rhs(y,t,mu):
5 return [ y[1], mu*(1-y[0]**2)*y[1] - y[0]]
6
10 mu=1.0
11 t_final = 15.0 if mu < 10 else 4.0*mu
12 n_points = 1001 if mu < 10 else 1001*mu
13 t=np.linspace(0,t_final,n_points)
14 y0=np.array([2.0,0.0])
15 y,info=odeint(rhs,y0,t,args=(mu,),Dfun=jac,full_output=True)
16
The syntax in lines 11–12 was introduced at the end of Section 3.6. These lines re-
flect that, as explained earlier, the period depends on the parameter μ. Notice that the
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.3 The odeint Function 159
call to odeint in line 15 takes two further named arguments. The first, Dfun, tells the
integrator where to find the Jacobian function. The second sets full_output. While
the integrator is running, it collects various statistics which can aid understanding of
what precisely it is doing. If full_output is set to True (the default is False), then
these data are made accessible as a dictionary via the second identifier on the left-hand
side of line 15. For a complete list of available data, the reader should consult the doc-
string of the function odeint. Here we have chosen to display the “number of Jacobian
evaluations”, which is available as info[’nje’]. The reader is encouraged to experi-
ment with this snippet, gradually increasing the value of the parameter mu in line 10.
For small to moderate values of the parameter μ (and default values for the error toler-
ances), the integrator does not need to use implicit algorithms, but they are needed for
μ > 15.
Of course, the visualization scripts introduced earlier can be used in this context. The
reader is encouraged to experiment here, in order to become familiar with the van der
Pol equation, or odeint’s capabilities, or both. Note that increasing the parameter μ
implies increasing the period, see equation (8.15), and so increasing the final time and
the number of points to be plotted.
where β, ρ and σ are constants, and we specify initial conditions at, say, t = 0. Lorenz
originally studied the case where σ = 10 and β = 8/3, and this practice is widely
followed, allowing only ρ to vary. For smallish values of ρ, the behaviour of solutions
is predictable, but once ρ > ρH ≈ 24.7 the solutions become aperiodic. Further, they
exhibit very sensitive dependence on initial conditions. Two solution trajectories corre-
sponding to slightly different initial data soon look quite different. The following self-
contained code snippet can be used to investigate the aperiodicity.
1 import numpy as np
2 from scipy.integrate import odeint
3
4 def rhs(u,t,beta,rho,sigma):
5 x,y,z = u
6 return [sigma*(y-x), rho*x-y-x*z, x*y-beta*z]
7
8 sigma=10.0
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
160 Ordinary Differential Equations
9 beta=8.0/3.0
10 rho1=29.0
11 rho2=28.8
12
13 u01=[1.0,1.0,1.0]
14 u02=[1.0,1.0,1.0]
15
16 t=np.linspace(0.0,50.0,10001)
17 u1=odeint(rhs,u01,t,args=(beta,rho1,sigma))
18 u2=odeint(rhs,u02,t,args=(beta,rho2,sigma))
19
27 plt.ion()
28 fig=plt.figure()
29 ax=Axes3D(fig)
30 ax.plot(x1,y1,z1,’b-’)
31 ax.plot(x2,y2,z2,’r:’)
32 ax.set_xlabel(’x’)
33 ax.set_ylabel(’y’)
34 ax.set_zlabel(’z’)
35 ax.set_title(’Lorenz equations with rho = %g, %g’ % (rho1,rho2))
Here x, y and z are packaged as a three-vector u. However, for clarity they are unpacked
locally in lines 5, 20 and 21. Two solution trajectories whose ρ parameters vary only
slightly are plotted as solid and dotted curves in Figure 8.2.
Notice that, if ρ > 1, there are equilibrium points (zero velocity) at
(0, 0, 0), (± β(ρ − 1), ± β(ρ − 1), ρ − 1),
while if 0 < ρ < 1 the only equilibrium point is the origin. Although the solution does
not approach any equilibrium point in particular, it does seem to wind its way around the
two non-trivial equilibrium points. From nearby one of these points, the solution spirals
out slowly. When the radius gets too large, the solution jumps into a neighbourhood of
the other equilibrium, where it begins another outward spiral, and the process repeats
itself. The resulting picture looks somewhat like the wings of a butterfly. See again
Figure 8.2.
Note that there are significant regions of only solid or only dotted curves. The two
solution curves, although initially close, diverge pointwise as time increases. This is
easier to see in a colour plot. The “butterfly structure”, however remains intact. Much
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.4 Two-Point Boundary Value Problems 161
Figure 8.2 Two solution curves for the Lorenz equations, whose ρ parameter varies slightly. The
solid and dotted regions show where the solutions differ markedly pointwise. However, the
“butterfly structure” appears to be stable.
of our knowledge of the Lorenz and related systems is based on numerical experiments.
There is a detailed discussion in Sparrow (1982).
8.4.1 Introduction
Here is a very simple model for a two-point boundary value problem:
y (x) = f (x, y, y ) for a < x < b with y(a) = A, y(b) = B. (8.18)
(We have changed the independent variable from t to x to accord with historical conven-
tion.) As we shall see, this is a global problem and hence much harder than the initial
value problem of Section 8.1, which is local. Existence theorems are much harder to
formulate, let alone prove, and numerical treatments are not so straightforward as those
for the earlier case.
Consider, e.g., the simple linear example
y (x) + h(x)y(x) = 0, 0 < x < 1, with y(0) = A, y(1) = B. (8.19)
Because of the linearity, we might choose to use a “shooting approach”. We first solve
two initial value problems (see Section 8.1)
y1 (x) + h(x)y1 (x) = 0, x > 0, y1 (0) = 1, y1 (0) = 0,
y2 (x) + h(x)y2 (x) = 0, x > 0, y2 (0) = 0, y2 (0) = 1.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
162 Ordinary Differential Equations
Linearity implies that the general solution is y(x) = C1 y1 (x) + C2 y2 (x) where C1 and C2
are constants. We satisfy the boundary condition at x = 0 by requiring C1 = A.
Now the boundary condition at x = 1 requires Ay1 (1)+C2 y2 (1) = B. If y2 (1) 0, then
there is a unique solution for C2 , and the problem has a unique solution. However, if
y2 (1) = 0, then there are infinitely many solutions for C2 if Ay1 (1) = B and no solutions
at all otherwise.
We should note from this that the existence and uniqueness of solutions of boundary
value problems is far less clear cut than for the initial value problem, and depends in
an intrinsic way on the behaviour of the solution throughout the integration interval.
We need a new approach. The textbook by Ascher et al. (1995) offers a careful balance
between theory and application, and is recommended for investigating further the back-
ground concepts of this topic. A selection of that material, treated at a more elementary
level, can be found in Ascher et al. (1998).
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.4 Two-Point Boundary Value Problems 163
a trivial differential equation to the system, v (x) = 0 with v(0) = u(0). The system
now has two unknowns Y = (u, v) and is of order 3, and the three separated boundary
conditions for Z = (u, u , v) are u (0) = 1, u(0) − v(0) = 0 and u(1) + v(1) = 0 at z0 = 0,
z1 = 0 and z2 = 1 respectively.
This trick can be used to deal with unknown parameters and/or normalization condi-
tions. Again consider a simple-at-first-glance eigenvalue example
1
u (x) + λu(x) = 0, u(0) = u(1) = 0, with u2 (s) ds = 1. (8.22)
0
x
We introduce two auxiliary variables v(x) = λ and w(x) = 0 u2 (s) ds. Thus our un-
knowns are Y = (u, v, w) with Z = (u, u , v, w). There are three differential equations of
orders 2, 1, 1, namely
u (x) = −u(x)v(x), v (x) = 0, w (x) = u2 (x), (8.23)
and four separated boundary conditions,
u(0) = 0, w(0) = 0, u(1) = 0, w(1) = 1. (8.24)
Thus a large class of boundary value problems can be coerced into our standard form
(8.20) and (8.21).
The example above illustrates another facet of the boundary value problem. √ It is easy
to see that problem (8.22) has a countably infinite set of solutions un (x) = 2 sin(nπx)
with λ = λn = (nπ)2 for n = 1, 2, . . .. Although the u-equation is linear in u, it is
non-linear
√ in Y, and the eigenvalues are the solutions of a non-linear equation, here
sin( λ) = 0. Note also that non-linear equations are not in general soluble in closed
form, and can have many solutions. In such situations, solution techniques usually in-
volve iterative methods, and we need to be able to specify which solution we are inter-
ested in. For example we might supply a more or less informed guess as to the unknown
solution’s behaviour.
Shooting methods “guess” starting values for initial value problems. However, and
especially for non-linear problems, the wrong guess frequently produces trial solutions
which blow up before the distant boundary is reached. Thus we need to consider also
different techniques for the numerical solution of boundary value problems. We are
considering the interval a x b, so we represent this numerically by a discrete grid
a x0 x1 · · · x M−1 b. This grid might, perhaps, include the grid of boundary
points {z j } introduced above. Next we face two related issues: (i) how do we represent
the unknown functions on the grid; and (ii) how do we approximate the differential
equations and boundary conditions on the grid?
Clearly, choices which are tailored to one particular problem may be less than optimal
for another. Given the diversity of problems that are being considered, many may choose
to select the method apposite to their problem. This assumes that they know, or can get
good advice on, the optimal choice. For others, a black box solution, while very rarely
providing the optimal solution, may generate a reliable answer quickly, and that is the
approach we adopt here. There are many black box packages described in the literature,
usually available either as Fortran or C++ packages. One of the more widely respected
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
164 Ordinary Differential Equations
packages is COLNEW, see Bader and Ascher (1987), which is described extensively in
appendix B of Ascher et al. (1995). The Fortran code has been given a Python wrapper
in the scikit package scikits.bvp1lg. (Scikit packages were discussed in Section
4.9.2.) Its installation, which requires an accessible Fortran compiler, is described in
Section A.5.
for which the exact solution is u(x) = − sin x. The following code snippet can be used
to obtain and plot the numerical solution. Note that, for the sake of both brevity and
clarity, we have kept the plotting instructions to the bare minimum.
1 import numpy as np
2 import scikits.bvp1lg.colnew as colnew
3
4 degrees=[2]
5 boundary_points=np.array([0,np.pi])
6 tol=1.0e-8*np.ones_like(boundary_points)
7
8 def fsub(x,Z):
9 """The equations"""
10 u,du=Z
11 return np.array([-u])
12
13 def gsub(Z):
14 """The boundary conditions"""
15 u,du=Z
16 return np.array([u[0],du[1]-1.0])
17
18 solution=colnew.solve(boundary_points,degrees,fsub,gsub,
19 is_linear=True,tolerances=tol,
20 vectorized=True,maximum_mesh_size=300)
21
22 x=solution.mesh
23 u_exact=-np.sin(x)
24
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.4 Two-Point Boundary Value Problems 165
30 ax.plot(x,solution(x)[:,0],’b.’)
31 ax.plot(x,u_exact,’g-’)
Here line 2 imports the boundary value problem solver package colnew. There is pre-
cisely one differential equation of degree 2 and this is specified by the Python list in
line 4. There are two boundary points, and these are given as a list coerced to an array
in line 5. We need to specify an array of error tolerances that we will permit for each of
the boundary points, and an arbitrary choice for this is made in line 6.
We write the single equation as u (x) = −u(x) and we specify the right-hand side in
the function fsub. Its arguments are x and the enlarged vector of dependent variables
Z = {u, u }, which (for convenience) we unpack in line 10. Note that we must return the
single value −u as an array, which involves first packing it into a list, line 11, to ensure
that when fsub is called by colnew with arrays as arguments then the output is an array
of the appropriate size. Similar considerations apply to the boundary conditions which
we first rewrite as u(0) = 0 and u (π) − 1 = 0. The function gsub returns them as an
array coerced from a list in line 16. Note that the array subscripts refer to the points
specified in boundary_points. Thus u[0] refers to u(0) but du[1] refers to u (π). It
is important to get the syntax right for these functions, for otherwise the error messages
are somewhat obscure.
After this preliminary work, we call the function colnew.solve in line 18. The first
four arguments are mandatory. The remainder are keyword arguments, and the ones we
have used here should suffice for simple problems. For a full list of the optional ar-
guments, we should examine the function’s docstring in the usual way. The remaining
lines show how to use the output from colnew.solve to which we assigned the identi-
fier solution. The array of x-values for which the solution was determined is stored as
solution.mesh, to which we assign the identifier x in line 22. Now the enlarged so-
lution vector Z = {u, u } is available as the two-dimensional array solution(x)[ , ].
The second argument specifies the Z-component (u or du), while the first labels the
point at which it is computed. Thus the second argument of plt.plot in line 30 returns
a vector of u-values.
This is clearly a great deal of effort for such a simple problem. Fortunately, the work
load hardly changes as the difficulty increases, which means that much of that snippet
can be cannibalized, as the next two examples show.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
166 Ordinary Differential Equations
1 import numpy as np
2 import scikits.bvp1lg.colnew as colnew
3
4 degrees=[2,1,1]
5 boundary_points=np.array([0.0, 0.0, 1.0, 1.0])
6 tol=1.0e-5*np.ones_like(boundary_points)
7
8 def fsub(x,Z):
9 """The equations"""
10 u,du,v,w=Z
11 ddu=-u*v
12 dv=np.zeros_like(x)
13 dw=u*u
14 return np.array([ddu,dv,dw])
15
16 def gsub(Z):
17 """The boundary conditions"""
18 u,du,v,w=Z
19 return np.array([u[0],w[1],u[2],w[3]-1.0])
20
21
22 guess_lambda=100.0
23 def guess(x):
24 u=x*(1.0/3.0-x)*(2.0/3.0-x)*(1.0-x)
25 du=2.0*(1.0-2.0*x)*(1.0-9.0*x+9.0*x*x)/9.0
26 v=guess_lambda*np.ones_like(x)
27 w=u*u
28 Z_guess=np.array([u,du,v,w])
29 f_guess=fsub(x,Z_guess)
30 return Z_guess,f_guess
31
32
33 solution=colnew.solve(boundary_points,degrees,fsub,gsub,
34 is_linear=False,initial_guess=guess,
35 tolerances=tol,vectorized=True,
36 maximum_mesh_size=300)
37
38 # plot solution
39
43 plt.ion()
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.4 Two-Point Boundary Value Problems 167
44 x=solution.mesh
45 u_exact=np.sqrt(2)*np.sin(3.0*np.pi*x)
46 plt.plot(x,solution(x)[:,0],’b.’,x,u_exact,’g-’)
47 print ‘‘Third eigenvalue is %16.10e .’’ % solution(x)[0,2]
Here the new feature is the initial guess, lines 22–30. The function guess(x) is required
to return the initial guess for both the enlarged solution vector Z and the corresponding
right-hand sides. Line 24 specifies u as a suitable quartic polynomial, and line 25 its
derivative. The rest of the code should be self-explanatory. The third eigenvalue is ob-
tained as 88.826439647, which differs from the exact value 9π2 by a factor 1 + 4 × 10−10 .
4 degrees=[2]
5 boundary_points=np.array([0.0, 1.0])
6 tol=1.0e-8*np.ones_like(boundary_points)
7
14
15 def gsub(Z):
16 """The boundary conditions"""
17 u,du=Z
18 return np.array([u[0],u[1]])
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
168 Ordinary Differential Equations
19
20 def initial_guess(x):
21 """Initial guess depends on parameter mu"""
22 u=mu*x*(1.0-x)
23 du=mu*(1.0-2.0*x)
24 Z_guess=np.array([u, du])
25 f_guess=fsub(x,Z_guess)
26 return Z_guess,f_guess
27
28 lamda=1.0
29 mu=0.2
30 solution=initial_guess
31
32 solution=colnew.solve(boundary_points,degrees,fsub,gsub,
33 is_linear=False,initial_guess=solution,
34 tolerances=tol,vectorized=True,
35 maximum_mesh_size=300)
36
37 # plot solution
38
42 plt.ion()
43 x=solution.mesh
44 plt.plot(x,solution(x)[:,0],’b.’)
45 plt.show(block=False)
This is very similar to the code snippet in Section 8.4.4, but with two changes. As
explained in Section 3.8.8, lambda has a reserved meaning. It cannot be used as an
identifier and so we use lamda in line 11 for the parameter in the equation (8.26).
Next note that we have offered a guess for the solution in lines 20–26. If we run this
snippet with the parameter values λ = 1 and μ = 0.2 in lines 28 and 29, we obtain
a smooth solution. However, if we retain λ = 1 but set μ = 20, we obtain a different
smooth solution. (Examine the maximum values of both solutions!) If we change λ, say
λ = 5, then no solution is generated! It would seem that λ is not a good parameter for
delineating the solutions.
Experience, including the parenthetical remark above, suggests that a better parame-
ter for describing the solutions might be a norm of the solution. We therefore consider
an enlarged problem with extra dependent variables,
x
v(x) = λ, w(x) = u2 (s) ds,
0
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.4 Two-Point Boundary Value Problems 169
Figure 8.3 The eigenvalue λ for the Bratu problem as a function of the norm γ of the solution.
1 import numpy as np
2 import scikits.bvp1lg.colnew as colnew
3
4 degrees=[2,1,1]
5 boundary_points=np.array([0.0,0.0,1.0,1.0])
6 tol=1.0e-8*np.ones_like(boundary_points)
7
8 def fsub(x,Z):
9 """The equations"""
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
170 Ordinary Differential Equations
10 u,du,v,w=Z
11 ddu=-v*np.exp(u)
12 dv=np.zeros_like(x)
13 dw=u*u
14 return np.array([ddu,dv,dw])
15
16
17 def gsub(Z):
18 """The boundary conditions"""
19 u,du,v,w=Z
20 return np.array([u[0],w[1],u[2],w[3]-gamma])
21
22 def guess(x):
23 u=0.5*x*(1.0-x)
24 du=0.5*(1.0-2.0*x)
25 v=np.zeros_like(x)
26 w=u*u
27 Z_guess=np.array([u,du,v,w])
28 f_guess=fsub(x,Z_guess)
29 return Z_guess,f_guess
30
31 solution=guess
32 gaml=[]
33 laml=[]
34
45 # plot solution
46 import matplotlib.pyplot as plt
47 %matplotlib notebook
48
49 plt.ion()
50 fig = plt.figure()
51 ax = fig.add_subplot(111)
52 ax.plot(gaml, laml)
53 ax.set_xlabel(r’$\gamma$’, size=20)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.5 Delay Differential Equations 171
54 ax.set_ylabel(r’$\lambda$’, size=20)
55 ax.grid(b=True)
We see from Figure 8.3 that λ is a smooth function of γ with a maximum at γ ≈ 0.7. If
we rerun the code snippet for γ ∈ [0.65, 0.75] (by modifying line 35), we start to home
in on the maximum at γ = γc . We can get the maximum value of λ via np.max(laml)
and the code estimates λc = 3.513830717996. For λ < λc , there are two solutions with
different values of γ, but if λ > λc , then there is no solution at all. This confirms and
adds detail to our first investigation.
In fact, we can investigate the analytic solution of (8.26). If we set v(x) = u (x), then
u = log(−v /λ) and we find that v = vv = 12 (v2 ) , so that v − 12 v2 = k, a constant.
First, we assume k < 0, setting k = −8ν2 . (The analysis for k > 0 is similar but leads to
negative eigenvalues λ, and the case k = 0 leads to singular solutions for u(x).) Thus we
need to solve
v (x) − 12 v2 (x) = −8ν2 ,
and the general solution is
v(x) = −4ν tanh(2ν(x − x0 )),
where x0 is an arbitrary constant. This implies
u(x) = −2 log [cosh(2ν(x − x0 ))] + const.
We use the constants to fit the boundary conditions u(0) = u(1) = 0, finding
⎡ ⎤
⎢⎢⎢ cosh(2ν(x − 12 )) ⎥⎥⎥
u(x) = −2 log ⎣ ⎢ ⎥⎦ .
cosh ν
Next we determine λ = −u / exp(u) as
ν 2
λ = λ(ν) = 8.
cosh ν
Clearly, λ(ν) > 0 for ν > 0 and has a single maximum at ν = νc where νc is the single
positive root of coth ν = ν. We found in Section 4.9.1 that νc ≈ 1.19967864026, and so
0 < λ < λc where λc = λ(νc ) ≈ 3.51383071913. The numerical estimate above differs
by a few parts in 109 , a confirmation of its accuracy.
This has been a very brief survey of what Python can do with boundary value prob-
lems. Many important problems, e.g., equations on infinite or semi-infinite domains,
have been omitted. They are, however, covered in Ascher et al. (1995), to which the
interested reader is directed.
Delay differential equations arise in many scientific disciplines and in particular in con-
trol theory and in mathematical biology, from where our examples are drawn. For a
recent survey, see, e.g., Erneux (2009). Because they may not be familiar to all users,
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
172 Ordinary Differential Equations
we start by considering in some detail a very simple case. Readers can find a systematic
treatment in textbooks, e.g., Driver (1997).
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.5 Delay Differential Equations 173
Clearly, we can generalize (8.27) to a system of equations, we can include many dif-
ferent delays τ1 , τ2 , . . ., and can even allow the delay to depend on time and state,
τ = τ(t, x). As we have suggested, delay differential equations have a remarkably rich
structure, and are not a simple extension of the initial value problem for ordinary dif-
ferential equations discussed in Section 8.1, and for this reason the software used there
to construct numerical solutions is of little use here. For a useful introduction to the
numerical solution of delay differential equations, see Bellen and Zennaro (2003).
It is extremely difficult to construct a robust “black box” integrator, like the odeint
function of Section 8.1, which can handle general delay differential equations. Fortu-
nately, most of those encountered in mathematical biology have one or more constant
delays and no state-dependent ones. In this simpler case, there are a number of robust
algorithms, see Bellen and Zennaro (2003) for a discussion. A commonly used integra-
tor, which goes under the generic name dde23, is due to Bogacki and Shampine (1989).
(This is made up of Runge–Kutta integrators of orders 2 and 3 and cubic interpolators.)
Because of its complexity, it is usually used in “packaged” form, as, for example, within
Matlab. Within Python, it is accessible via the pydelay package, which can be down-
loaded from its website.3 Instructions for installing such packages are given in Section
A.5.
The pydelay package contains comprehensive documentation and some well-chosen
example codes, including, inter alia, the Mackey–Glass equation from mathematical
biology. Here we shall consider first a superficially simpler example which, however,
exhibits surprising behaviour. But first we need to say something about how the package
works. Python is ultimately based on the C language. We saw in Chapter 4 how NumPy
could convert Python list operations into C array operations “on the fly”, dramatically
increasing Python’s speed. Buried in SciPy is the swig tool. This allows the user to
pass to the interpreter valid C-code via a string of characters (often in docstring format
for substantial chunks) which will be compiled “on the fly” and executed. Using swig
proactively is not easy for the beginner, and most scientific users interested in using
compiled code will gain more utility by exploring the f2py tool discussed in Section
9.7. The developer of pydelay has offered a clever interface between swig and the casual
user. The user needs to input the equations (including various parameters) as strings to
enable the package to write a valid C-program, which is then compiled and hidden away
in a temporary file. Next, data required to execute the dde23-program must be provided.
Finally, after execution, we need to recover the output as Numy arrays. In Python, these
input/output operations are handled seamlessly by dictionaries of objects. Recall from
Section 3.5.7 that a dictionary member consists of a pair. The first, the key, must be
an immutable object, usually a string, which identifies the member. The second, the
content, can be of any valid type. This background is essential for understanding the
code snippets in the remainder of this section.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
174 Ordinary Differential Equations
Figure 8.4 Numerical solution of the logistic differential delay equation (8.30) for three choices
of the delay τ. The left graph is x(t) as a function of time t. On the right is a quasi-phase plane
plot, x(t − τ) versus x(t).
The dimensionless logistic equation was mentioned earlier (1.3) as a simple example
in population dynamics. Now consider its delay differential equation counterpart,
dx
(t) = x(t) 1 − x(t − τ) , (8.30)
dt
where the constant τ is non-negative. For the biological background suggesting this
equation, see, e.g., Murray (2002). For τ = 0, we recover the ordinary differential equa-
tion whose solutions have a very simple behaviour. There are two stationary solutions
x(t) = 0 and x(t) = 1, of which the first is a repeller and the second an attractor. For
arbitrary initial data x(0) = x0 > 0, the solution tends monotonically to the attractor as
t → ∞.
For small positive values of τ, we might reasonably expect this behaviour to persist.
Suppose we consider initial data
x(t) = x0 for − τ t 0. (8.31)
Then a linearized analysis shows that for 0 < τ < e−1 this is indeed the case, see row 1 of
Figure 8.4. However, for e−1 < τ < 12 π the convergence to the attractor y = 1 becomes
oscillatory, as can be seen in row two of Figure 8.4. At τ = 12 π, a Hopf bifurcation
occurs, i.e., the attractor becomes a repeller and the solution tends to a limit cycle. This
is evident in row three of Figure 8.4.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.5 Delay Differential Equations 175
The supercritical case τ > 12 π is very surprising in that such a simple model can admit
periodic behaviour. Further, for large choices for τ, the minimum value of x(t) can be
extremely small. In population dynamics, we might interpret this as extermination, sug-
gesting that the population is wiped out after one or two cycles. For further discussion
of the interpretation of these solutions, see, e.g., Erneux (2009) and Murray (2002).
Each row of Figure 8.4 was produced with the aid of the following code.
1 import numpy as np
2 import matplotlib.pyplot as plt
3 %matplotlib notebook
4 from pydelay import dde23
5
6 t_final=50
7 delay=2.0
8 x_initial=0.1
9
10 equations={’x’ : ’x*(1.0-x(t-tau))’}
11 parameters={’tau’ : delay}
12 dde=dde23(eqns=equations,params=parameters)
13 dde.set_sim_params(tfinal=t_final,dtmax=1.0,
14 AbsTol=1.0e-6,RelTol=1.0e-3)
15 histfunc={’x’: lambda t: x_initial}
16 dde.hist_from_funcs(histfunc,101)
17 dde.run()
18
19 t_vis=0.1*t_final
20 sol=dde.sample(tstart=t_vis+delay,tfinal=t_final,dt=0.1)
21 t=sol[’t’]
22 x=sol[’x’]
23 sold=dde.sample(tstart=t_vis,tfinal=t_final-delay,dt=0.1)
24 xd=sold[’x’]
25
26 plt.ion()
27 fig = plt.figure()
28 ax1=fig.add_subplot(121)
29 ax1.plot(t, x)
30 ax1.set_xlabel(’t’)
31 ax1.set_ylabel(’x(t)’)
32
33 ax2=fig.add_subplot(122)
34 ax2.plot(x, xd)
35 ax2.set_xlabel(’tx’)
36 ax2.set_ylabel(’x(t-tau)’)
Lines 1–3 are routine. Line 4 imports dde23 as well as SciPy and various other modules.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
176 Ordinary Differential Equations
Lines 6–8 just declare the length of the evolution, the delay parameter τ and the initial
value x0 , and need no explanation.
Lines 10–17 contain new ideas. We have to tell dde23 our equations, supplying them
in valid C-code. Fortunately, most arithmetic expressions in core Python are acceptable.
Here there is precisely one equation and we construct a dictionary equations with
precisely one member. The equation is for dx/dt and so the member key is ’x’, and
the value is a Python string containing the formula for dx/dt, line 9. The equation
involves a parameter tau, so we need a second dictionary parameters to supply it. The
dictionary contains an element for each member, whose key is the string representing
the parameter, and the value is the numerical value of the parameter, line 11. Next line 12
writes C-functions to simulate this problem. Finally, we have to supply input parameters
to it. Examples of the usual numerical ones are given in lines 13–14. Here the “initial
data” corresponding to x are given by (8.31) and we construct a third dictionary with
one member to supply them, using the anonymous function syntax from Section 3.8.8
in line 15 and supply them to the C-code in line 16. Finally, line 17 runs the background
C-code.
Next, we have to extract the solution. Usually we do not know the precise initial data,
and will therefore choose to display the steady state after (we hope) the initial transient
behaviour has disappeared. The purpose of t_vis is to choose a starting time for the
figures. The solution generated by the C-program is available, but at values of t which
have been chosen by dde23, and these will not usually be evenly spaced. The purpose
of the sample function in line 20 is to perform cubic interpolation and to return the data
evenly spaced in t for the parameters specified in the arguments to the function. In the
right-hand column of Figure 8.4, we show quasi-phase plane plots of x(t −τ) versus x(t).
Thus, in line 20, we choose the range to be t in [tvis + τ, tfinal ], but in line 23 we use t in
[tvis , tfinal − τ], both with the same spacing. The function sample returns a dictionary.
Then line 21 extracts the member with key ’t’, which turns out to be NumPy array of
evenly spaced t-values. Lines 22 and 24 return x(t) and x(t − τ) values as arrays. More
arguments can be supplied to the dde functions. For further information, see the very
informative docstrings.
Finally, in lines 26–36 we draw x(t) as a function of t and, in a separate figure, a
plot of x(t − τ) against x(t) a “pseudo-phase-plane portrait”. The output of this snippet
corresponds to the last row in Figure 8.4. The entire compound figure was constructed
using the techniques of Section 5.10, and is left as an exercise for the reader.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.5 Delay Differential Equations 177
form, it is
dx x(t − τ)
(t) = a − bx(t), (8.32)
dt 1 + (x(t − τ))m
where the constants a, b, m and τ are non-negative. For −τ t 0, we shall choose
initial data x(t) = x0 as before.
Because of its importance, example code to solve it is included in the pydelay pack-
age. However, if you have already experimented with the code snippet for the logistic
delay differential equation in the previous subsection, it is easiest merely to plagiarize
and modify a copy of that, as we now show.
Clearly, we need to change the constants, so lines 6–8 of the previous snippet should
be replaced by the arbitrary values
t_final=500
a=2.0
b=1.0
m=7.0
delay=2.0
x_initial=0.5
Next we need to change the equation and parameters. Now, dde23 knows about the C
math.h library, so that cos u would be encoded as cos(u) etc. First consider the expo-
nentiation in (8.32). In Python (and Fortran), we would represent uv by u**v. However,
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
178 Ordinary Differential Equations
Figure 8.6 Quasi-phase plane portraits for the Mackey–Glass equation. The parameters used are
specified in Figure 8.5, except that m = 7 there is changed to the values shown. At m = 7, the
solution has period 1. At m = 7.4, this has doubled, and at m = 8.7, the period has quadrupled.
Increasing m further leads to chaos, as shown at m = 9.6. However, larger values of m lead again
to regular motion. At m = 9.71, the solution has period 3, but this doubles if m = 9.75, and chaos
ensues again at m = 10. Larger values of m lead again to regular motion. Note that the actual
transitions between different behaviours take place at values of m intermediate to those shown.
Next we choose to examine the late stages of the trajectories and so change line 19 to
t_vis = 0.95*t_final
The amended snippet was used to produce each of the two components of Figure 8.5,
which exhibits limit cycle behaviour qualitatively similar to the last row of Figure 8.4.
Because the Mackey–Glass equation contains four adjustable parameters a complete
exploration of its properties is a non-trivial undertaking. Here, following the original
authors, we shall experiment with changing m while keeping the other parameters fixed.
We display the quasi-phase plane plots (after running the evolution for rather longer
times than in the snippet above) in Figure 8.6. The details are specified in the figure
caption, but we see that the limit cycle behaviour exhibits period doubling and chaos.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.6 Stochastic Differential Equations 179
Biologists may prefer to examine the corresponding wave forms, which will require
adjustment of the t_vis parameter to limit the number of periods displayed.
It would be possible to supply examples that are more complicated, such as those
enclosed in the pydelay package, but the interested reader should be able to build on the
two exhibited here.
The previous three sections have dealt with scientific problems where the models can
be fairly precisely described, and the emphasis has been on methods with high orders
of accuracy. There exist many areas of scientific interest, e.g., parts of mathematical bi-
ology and mathematical finance, where such precision is simply unavailable. Typically,
there are so many sources of influence that they cannot be counted and we need to con-
sider stochastic evolution. For a heuristic but wide-ranging survey of the possibilities
see, e.g., Gardiner (2009). We shall specialize here to autonomous stochastic differen-
tial equations. A careful treatment of the theory can be found in, e.g., Øksendal (2003),
and for the underlying theory of numerical treatments, the standard reference is Kloe-
den and Platen (1992). However, this author has found the numerical treatment provided
by Higham (2001) particularly enlightening, and some of his ideas are reflected in the
presentation here. Of necessity, Higham omits many of the mathematical details, but
for these the stimulating lectures of Evans (2013) are highly recommendable. In this
sketch of the theory, we shall also take a very heuristic approach, relying on the cited
references to supply the theoretical justification.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
180 Ordinary Differential Equations
1 import numpy as np
2 import numpy.random as npr
3
4 T=1
5 N=500
6 t,dt=np.linspace(0,T,N+1,retstep=True)
7 dW=npr.normal(0.0,np.sqrt(dt),N+1)
8 dW[0]=0.0
9 W=np.cumsum(dW)
10
15 plt.plot(t,W)
16 plt.xlabel(’t’)
17 plt.ylabel(’W(t)’)
18 #plt.title(’Sample Wiener Process’,weight=’bold’,size=16)
The random variables are generated in line 7. The arguments to be supplied to the
normal function in its simplest form are the mean, standard deviation and the shape of
the required output array. Here we have generated precisely one more random number
than we require, so that we can change the first value to zero in line 8. This will ensure
that W(0) = 0. Finally, we generate W(t) as a cumulative sum of the increments in
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.6 Stochastic Differential Equations 181
line 9. (The function cumsum was introduced in Section 4.6.2.) The rest of the snippet
is routine, and we show one actual output in Figure 8.7. “Almost surely”, your attempt
will not be the same. Why?
We know that the expectation value of dW is dW = 0, and its variance is (dW 2 ) = Δt.
This suggests that the Wiener process W(t), while continuous, is nowhere differentiable,
since dW/Δt will diverge as Δt → 0. (In fact, it can be shown “almost surely”, i.e., with
probability 1, that W(t) is indeed nowhere differentiable.) Thus we may talk about the
differential dW(t) but not about dW(t)/dt.
Consider next the autonomous deterministic initial value problem
We now introduce a “noise” term on the right-hand side of equation (8.34) and replace
the deterministic x(t) by a random variable X(t) parametrized by t, writing our stochastic
differential equation (SDE) as
where the second integral on the right-hand side will be defined below. Assuming this
has been done, we say that X(t) given by equation (8.37) is a solution of the stochastic
differential equation (8.36).
Before proceeding further with this, we need to point out a trap for the unwary. Sup-
pose X(t) satisfies (8.36) and let Y(t) = f (X(t)) for some smooth function f . Naı̈vely,
we might expect
dY = f (X) dX = b f (X) dW + a f (X) dt, (8.38)
but, in general, this is incorrect! To see this, suppose we construct the first two terms in
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
182 Ordinary Differential Equations
4 T=1
5 N=1000
6 M=5000
7 t,dt=np.linspace(0,T,N+1,retstep=True)
8 dW=npr.normal(0.0,np.sqrt(dt),(M,N+1))
9 dW[ : ,0]=0.0
10 W=np.cumsum(dW,axis=1)
11 U=np.exp(- 0.5*W)
12 Umean=np.mean(U,axis=0)
13 Uexact=np.exp(t/8)
14
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.6 Stochastic Differential Equations 183
Figure 8.8 A function U(t) = exp(− 12 W(t)) of a discrete Brownian motion W(t). The dashed
curves are five sample paths. The solid curve gives the “exact” solution U(t) = exp( 18 t)
predicted by the Itô formula. Almost hidden beneath it is the dotted curve which maps the mean
over all of the 5000 samples, The maximum difference between the two for these Brownian
motion samples is about 1 × 10−2 .
18
19 fig=plt.figure()
20 ax=fig.add_subplot(111)
21
22 ax.plot(t,Umean,’b-’,label="mean of %d paths" % M)
23 ax.plot(t,Uexact,’r-’, label="exact " + r’$\langle U\rangle$’)
24 for i in range(5):
25 ax.plot(t,U[i, : ],’--’)
26
27 ax.set_xlabel(’t’)
28 ax.set_ylabel(’U’)
29 ax.legend(loc=’best’)
30
31 maxerr=np.max(np.abs(Umean-Uexact))
32 print "With %d paths and %d intervals the max error is %g" % \
33 (M,N,maxerr)
Most of this code should be familiar. The first novelty is line 8, where we generate M
instances of a Brownian motion, each with N+1 steps. dW is a two-dimensional array in
which the first index refers to the sample number and the second to the time. Line 9 sets
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
184 Ordinary Differential Equations
the initial increment for each sample to zero, and then line 10 generates the M Brownian
motion samples, by summing over the second index. U has of course the same shape as
W and dW. Line 11 generates the function values and line 12 calculates their average over
the M samples. (Beginners might like to consult the docstrings for cumsum and mean.)
Notice the total absence of explicit loops with the consequent tiresome, prone-to-error,
book-keeping details. In Figure 8.8, we plot the first five sample paths, the mean over
the M samples and the Itô suggestion U(t) = et/8 , and we also compute and print the
infinity norm of the error. As always, these code snippets are not tablets set in stone, but
suggestions for a basis for numerical experiments. Hacking them is the best way to gain
experience!
in the sense that the expectation values of both sides are the same. This turns out to be
well-defined but unfortunately, and unlike the deterministic case, it depends critically
on the choice of τk , or more precisely on the choice of f (τk ). To see this, we consider a
particular example, f (t) = W(t), and denote W(τk ) by W #k . We start from the algebraic
identity
# k ΔWk = 1 (Wk+1
W 2 # k − Wk )2 + (Wk+1 − W
− Wk2 ) − 12 ΔWk2 + (W # k )(W
#k − Wk ).
2
We now sum this identity from k = 0 to k = N − 1, and take expectation values. The
first term on the right produces 12 WN2 − W02 = 12 (W 2 (T 2 ) − W 2 (T 1 )). The second gives
− 12 k Δtk = − 12 (T 2 − T 1 ). Similarly, the third term produces k (τk − tk ) = λ(T 2 − T 1 ).
The expectation value of the final term gives zero, because the two time intervals are
disjoint and hence uncorrelated. Therefore
T2
W(t) dW(t) = 12 W 2 (T 2 ) − W 2 (T 1 ) + (λ − 12 )(T 2 − T 1 ), (8.40)
T1
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.6 Stochastic Differential Equations 185
to be solved numerically for t ∈ [0, T ]. We partition the t-interval into N equal sub-
intervals of length Δt = T/N, set tk = kΔt for k = 0, 1, . . . , N and abbreviate Xk = X(tk ).
Formally
tk+1 tk+1
Xk+1 = Xk + a(X(s)) ds + b(X(s)) dW(s). (8.42)
tk tk
where
dY 1 d2 Y dY
L[Y] = a(X) + 2 b(X) 2 , M[Y] = b(X) .
dX dX dX
Therefore
s s
Y(X(s)) = Y(Xk ) + L[Y(X(τ))] dτ + M[Y(X(τ))] dW(τ).
tk tk
Next replace Y(X) in the equation above first by a(X) and then by b(X), and substitute
the results into equation (8.42), obtaining
tk+1 $ s s %
Xk+1 =Xk + a(Xk ) + L[a(X(τ))] dτ + M[a(X(τ))] dW(τ) ds
tk tk tk
tk+1 $ s s %
+ b(Xk ) + L[b(X(τ))] dτ + M[b(X(τ))] dW(τ) dW(s).
tk tk tk
We rearrange this as
The Euler–Maruyama method consists of retaining just the first line of the expression
above,
Xk+1 = Xk + a(Xk )Δt + b(Xk )(Wk+1 − Wk ). (8.43)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
186 Ordinary Differential Equations
From the calculations in the previous subsection, we know how to compute the double
integral, in the Itô form, as
tk+1 s tk+1
dW(s) dW(τ) = (W(s) − Wk ) dW(s)
tk tk tk
= 2 (Wk+1 − Wk ) − 2 Δt −
1 2 2 1
Wk (Wk+1 − Wk )
= 2 (Wk+1 − Wk ) − Δt .
1 2
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.6 Stochastic Differential Equations 187
1 import numpy as np
2 import numpy.random as npr
3
4 # Set up grid
5 T=1.0
6 N=1000
7 t,dt=np.linspace(0,T,N+1,retstep=True)
8
22 # Analytic solution
23 Xanal=Xzero*np.exp((lamda-0.5*mu*mu)*t+mu*W)
24
25 # Milstein solution
26 Xmil=np.empty_like(t)
27 Xmil[0]=Xzero
28 for n in range(N):
29 Xmil[n+1]=Xmil[n]+dt*a(Xmil[n]) + dW[n+1]*b(Xmil[n]) + 0.5*(
30 b(Xmil[n])*bd(Xmil[n])*(dW[n+1]**2-dt))
31
34 plt.ion()
35 plt.plot(t,Xanal,’b-’,label=’analytic’)
36 plt.plot(t,Xmil,’g-.’,label=’Milstein’)
37 plt.legend(loc=’best’)
38 plt.xlabel(’t’)
39 plt.ylabel(’X(t)’)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
188 Ordinary Differential Equations
Figure 8.9 An instance of an analytic solution (8.49) of the equation (8.48) and its solution using
the Milstein algorithm over 1000 intervals. There is no discernible difference.
As can be seen in Figure 8.9, the Milstein algorithm generates a solution which ap-
pears, visually, to be very close to the analytic one. As an exercise, we might like to
modify lines 29 and 30 to produce the equivalent figure using the Euler–Maruyama
algorithm. We should be cautious though, for the figure relies on one instance of a
Brownian motion. Running the snippet repeatedly shows that the apparent convergence
can be better or worse than the example shown here.
Therefore, we next consider, numerically, the convergence rate of the two numerical
algorithms. The following code snippet investigates empirically the strong convergence
of the Euler–Maruyama and Milstein algorithms. For the sake of brevity, it contains
only minimal comments, but a commentary on the code follows the snippet.
1 import numpy as np
2 import numpy.random as npr
3
4 # Problem definition
5 M=1000 # Number of paths sampled
6 P=6 # Number of discretizations
7 T=1 # Endpoint of time interval
8 N=2**12 # Finest grid size
9 dt=1.0*T/N
10
11 # Problem parameters
12 lamda=2.0
13 mu=1.0
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.6 Stochastic Differential Equations 189
14 Xzero=1.0
15
47 # Do some plotting
48 import matplotlib.pyplot as plt
49 %matplotlib notebook
50 plt.ion()
51
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
190 Ordinary Differential Equations
58 label=’EM theoretical’)
59 Xmilerrmean=np.mean(Xmilerr,axis=0)
60 plt.plot(lDtvals,np.log10(Xmilerrmean),’bo’)
61 plt.plot(lDtvals,np.log10(Xmilerrmean),’b--’,label=’Mil actual’)
62 plt.plot(lDtvals,np.log10(Dtvals),’b-’,label=’Mil theoretical’)
63 plt.legend(loc=’best’)
64 plt.xlabel(r’$\log_{10}\Delta t$’,size=16)
65 plt.ylabel(r’$\log_{10}\left(\langle|X_n-X(\tau)|\rangle\right)$’,
66 size=16)
67
68 emslope=((np.log10(Xemerrmean[-1])-np.log10(Xemerrmean[0])) /
69 (lDtvals[-1]-lDtvals[0]))
70 print ’Empirical EM slope is %g’ % emslope
71 milslope=((np.log10(Xmilerrmean[-1])-
72 np.log10(Xmilerrmean[0])) / (lDtvals[-1]-lDtvals[0]))
73 print ’Empirical MIL slope is %g’ % milslope
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
8.6 Stochastic Differential Equations 191
Figure 8.10 The expectation value of the final error in the numerical solution of equation (8.48)
as a function of Δt. What matters here is the slopes of the “lines”. The Milstein method
converges like Δt, while the Euler–Maruyama one converges like (Δt)1/2 .
snippet could be shortened considerably, but it has been left in this form, so that it can
be hacked to compute and display different information, and handle equations that are
more complicated. The reader is strongly encouraged to use it as a starting point for
further experiments.
When treating the conventional initial and boundary value problems we placed con-
siderable emphasis on high-order, high-accuracy algorithms. This is justified because
there is an underlying assumption that we know both the equations and initial or bound-
ary data exactly. These assumptions are rarely justified for stochastic equations, so
higher-order methods are uncommon.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.010
9 Partial Differential Equations: A
Pseudospectral Approach
In this chapter, we present two rather different topics, which we have linked together.
First we shall study some initial value problems and initial boundary value problems,
where the forward-in-time integration is handled by the “method of lines”. We can treat
the spatial derivatives by one of two methods:
(i) finite differencing, the standard approach, which is discussed in every textbook, and
some aspects of which will be treated in the next chapter;
(ii) spectral methods, which, for smooth solutions, will give near exponential accuracy.
For simplicity, we shall study only scalar partial differential equations in one spatial
dimension, but everything can be generalized to systems of equations in two or more
dimensions. We look first at Fourier methods capable of handling problems with peri-
odic spatial dependence. Then in Section 9.6 we suggest a promising approach using
Chebyshev transforms for spatial dependences that are more general. Unfortunately,
there is no pre-existing Python black box package to implement this, but there is legacy
Fortran77 code, which we list in Appendix B.
The second main topic of this chapter is to present the NumPy f2py tool in Section
9.7, so that we can reuse the legacy code of Appendix B to construct Python functions
to implement the ideas of Section 9.6. If you are interested in reusing legacy code, then
you should study the ideas presented in Section 9.7, to find out how to do it, even if you
have no interest in Chebyshev transforms.
Essentially, all of the features we intend to describe here can be found in a single ex-
ample, Burgers’ equation. Suppose u(t, x) is defined for 0 t T and a x b and
satisfies
ut = −u u x + μ u xx , (9.1)
where μ > 0 is a constant parameter. Here we are using a common notation for partial
derivatives viz., u x = ∂u/∂x, u xx = ∂2 u/∂x2 etc. The first term on the right-hand side of
(9.1) represents self-convection, while the second implements diffusion. There are very
many physical processes where these two are the dominant effects, and they will be
governed by some variant of Burgers’ equation. In order to fix on a particular solution,
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
9.2 Method of Lines 193
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
194 PDEs: A Pseudospectral Approach
Now suppose we try to evolve u(t, x) forwards in time by using, e.g., the Euler method
with a time-step dt. We can compute u(t + dt, xn ) for interior points, but not u(t + dt, a)
(n = 0) or u(t + dt, b) (n = N). This is precisely where the boundary conditions (9.3)
come in, to furnish the missing values.
Of course, dt cannot be chosen arbitrarily. If we construct a linearized version of
(9.1), then a stability analysis, see, e.g., Section 8.2, shows that for explicit time-stepping
to be stable we need
dt < Cdx2 = O(N −2 ), (9.8)
where C is a constant of order unity. Since dx will be chosen small enough to satisfy ac-
curacy requirements, the restriction (9.8) on dt makes explicit time-stepping unattractive
in this context. Implicit time-stepping needs to be considered, but this too is unattractive
if there are significant non-linearities, because we have to solve a sequence of non-linear
equations.
Spectral methods offer a useful alternative to finite differencing. In order to illustrate the
ideas, we first consider a special case where the spatial domain [a, b] has been mapped
to [0, 2π] and we are assuming that u(t, x) is 2π-periodic in x. If we denote the Fourier
transform of u(t, x) with respect to x by & u(t, k), then, as is well known, the Fourier
transform of d u/dx is (ik) &
n n n
u(t, k) and so by Fourier inversion we can recover the spatial
derivatives of u(t, x). Thus, for Burgers’ equation (9.1), we need one Fourier transform
and two inversions to recover the two derivatives on the right-hand side. We need to turn
this into a spectral algorithm.
Suppose we represent u(t, x) for each t by function values at N equidistant points on
[0, 2π). We can construct the discrete Fourier transform (DFT) which, loosely speaking,
is the first N terms in the Fourier series expansion of u(t, x). We insert the appropriate
multiplier for each term and then compute the inverse DFT. The precise details are well
documented, see, e.g., Boyd (2001), Fornberg (1995), Hesthaven et al. (2007), Press
et al. (2007) or Trefethen (2000). Unfortunately, different authors employ different con-
ventions, and it is tedious and error-prone to translate between them. At first sight, this
approach might seem to be of academic interest only. Because each DFT is a linear
operation, it can be implemented as a matrix multiplication which requires O(N 2 ) oper-
ations, so the evaluation of the right-hand side of (9.1) requires O(N 2 ) operations, while
finite differencing requires only O(N).
If we know, or guess, that u(t, x) is smooth, i.e., arbitrarily many x-derivatives exist
and are bounded, then the truncation error in the DFT is o(N −k ) for arbitrarily large k.
In practice, our algorithm is likely to return errors of order O(10−12 ) for N ≈ 20. We
would need N ∼ 106 to achieve the same accuracy with a finite differencing approach.
If N has only small prime factors, e.g., N = 2 M , then the DFT and its inverse can
be computed using fast Fourier transform (FFT) techniques, which require O(N log N)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
9.4 Spatial Derivatives by Spectral Techniques 195
rather than O(N 2 ) operations. Of course, it is crucial here that u(t, x) be periodic as well
as smooth. If u(t, 0) u(t, 2π), then the periodic continuation would be discontinuous,
and the Gibbs phenomenon would wreck all of these accuracy estimates.
The question now arises as to whether to implement the DFT using matrix multipli-
cation or the FFT approach. If N 30, then matrix multiplication is usually faster. All
the techniques needed to construct a NumPy implementation have been outlined already
and the interested reader is invited to construct one.1 For larger values of N an efficient
implementation of the FFT approach is needed, and, since this involves important new
ideas, most of the second half of this chapter is devoted to it. Most of the standard FFT
routines for DFT operations are available in the scipy.fftpack module, and in par-
ticular there is a very useful function of the form diff(u,order=1,period=2*pi).
If u is a NumPy array representing the evenly spaced values of u(x) on [0, 2π), then the
function returns an array of the same shape as u containing the values of the first deriva-
tive for the same x-values. Higher-order derivatives and other periodicities are handled
by the parameters shown.
We consider a concrete example. Let f (x) = exp(sin x) for x ∈ [0, 2π), and com-
pute d f /dx on [0, 2π). The following code snippet implements a comparison of finite
differencing and the spectral approach.
1 import numpy as np
2 from scipy.fftpack import diff
3
4 def fd(u):
5 """ Return 2*dx* finite-difference x-derivative of u. """
6 ud=np.empty_like(u)
7 ud[1:-1]=u[2: ]-u[ :-2]
8 ud[0]=u[1]-u[-1]
9 ud[-1]=u[0]-u[-2]
10 return ud
11
12 for N in [4,8,16,32,64,128,256]:
13 dx=2.0*np.pi/N
14 x=np.linspace(0,2.0*np.pi,N,endpoint=False)
15 u=np.exp(np.sin(x))
16 du_ex=np.cos(x)*u
17 du_sp=diff(u)
18 du_fd=fd(u)/(2.0*dx)
19 err_sp=np.max(np.abs(du_sp-du_ex))
20 err_fd=np.max(np.abs(du_fd-du_ex))
21 print "N=%d, err_sp=%.4e err_fd=%.4e" % (N,err_sp,err_fd)
Table 9.1 shows the output from the code snippet. The infinity norm, i.e., the maximum
absolute value, of the finite differencing error decreases by a factor of about 4 for each
1 Boyd (2001) is one of many references which contain specific algorithms.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
196 PDEs: A Pseudospectral Approach
doubling of the number of points, i.e., the error = O(N −2 ), which is consistent with the
error estimate in (9.6). The spectral error squares for each doubling, until N is “large”,
i.e., N 30 here. This fast error decrease is often called exponential convergence. Then
two effects degrade the accuracy. With software producing arbitrary accuracy, we might
have expected the error to be O(10−30 ) for N = 64. But in normal use, most program-
ming languages handle floating-point numbers with an accuracy of about one part in
1016 , so we cannot hope to see the tiny errors. Also the eigenvalues of the equivalent
“differentiation matrix” become large, typically O(N 2p ) for pth-order differentiation,
and this magnifies the effect of rounding errors, as can be seen here for N 60.
Table 9.1 The maximum error in estimating the spatial derivative of exp(sin x) on [0, 2π) as a
function of N , the number of function values used. Doubling the number of points roughly
squares the spectral error, but decreases the finite difference (FD) error by a factor only of 4.
For very small spectral errors, i.e., very large N , this rule fails for artificial reasons explained
in the text.
It should be noted for spatially periodic problems that if u(t, x) is specified for fixed t
and x ∈ [0, 2π), then using the techniques of the previous section we can compute the
x-derivatives of u on the same interval, without needing boundary conditions like (9.3),
because u(t, 2π) = u(t, 0). This is used in lines 8 and 9 of the snippet above. Therefore,
when considering spatially periodic problems, only the initial value problem is relevant.
When we use the method of lines with, say, an explicit scheme, we need to consider
carefully the choice of the time interval dt we are going to use. Suppose, for simplicity,
we are considering a linear problem. Then the S[u] of (9.5) will be a linear functional
which we can represent by an N × N matrix A. We may order the (in general complex)
eigenvalues by the magnitude of their absolute values. Let Λ be the largest magnitude.
We know from the discussion in Section 8.2 that the stability of explicit schemes for the
time integration will require Λdt O(1). Now, if the highest order of spatial derivative
occurring in S[u] is p, then, for finite differencing, calculations show that Λ = O(N p ).
A common case is the parabolic equation for heat conduction where p = 2. Since
N 1 to ensure spatial accuracy, the stability requirement is dt = O(N −2 ). This may be
unacceptably small, and so implicit time integration schemes are often used. If instead
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
9.5 The IVP for Spatially Periodic Problems 197
we use the spectral scheme for evaluating spatial periodic derivatives, then it turns out
that a similar stability estimate obtains. (When we consider non-periodic problems later
in this chapter, then we can show that Λ = O(N 2p ), leading to dt = O(N −2p ), which,
at first sight, might rule out explicit time integration schemes.) However, we saw in
the previous section that the values of N needed for a given spatial accuracy are much
smaller than those required for finite differencing. Further, because of the phenomenal
accuracy achievable with even modest values of N, a small value of dt will be needed
anyway in order to achieve comparable temporal accuracy.
Once we have decided on a suitable size for dt we need to choose a, preferably ex-
plicit, scheme for the time integration process. Here the literature cited above has plenty
of advice, but for many purposes Python users do not need to make a choice, but can
rely instead on the odeint function of Section 8.3, as we show by means of a very
simple example. It is difficult to construct a non-linear initial value problem which is
periodic in space, so we consider linear advection,
for which the exact solution is u(t, x) = exp(sin(x − 2πt)). The following snippet carries
out a time integration and performs two common tasks, displaying the solution and
computing the final error. Most of the frills, comments, etc. have been omitted for the
sake of brevity.
1 import numpy as np
2 from scipy.fftpack import diff
3 from scipy.integrate import odeint
4 import matplotlib.pyplot as plt
5 from mpl_toolkits.mplot3d import Axes3D
6 %matplotlib notebook
7
8 def u_exact(t,x):
9 """ Exact solution. """
10 return np.exp(np.sin(x-2*np.pi*t))
11
12
17 N=32
18 x=np.linspace(0,2*np.pi,N,endpoint=False)
19 u0=u_exact(0,x)
20 t_initial=0.0
21 t_final=64*np.pi
22 t=np.linspace(t_initial,t_final,101)
23 sol=odeint(rhs,u0,t,mxstep=5000)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
198 PDEs: A Pseudospectral Approach
24
25 plt.ion()
26 fig=plt.figure()
27 ax=Axes3D(fig)
28 t_gr,x_gr=np.meshgrid(x,t)
29 ax.plot_surface(t_gr,x_gr,sol,alpha=0.5)
30 ax.elev,ax.azim=47,-137
31 ax.set_xlabel(’x’)
32 ax.set_ylabel(’t’)
33 ax.set_zlabel(’u’)
34
35 u_ex=u_exact(t[-1],x)
36 err=np.abs(np.max(sol[-1,: ]-u_ex))
37 print "With %d Fourier nodes the final error = %g" % (N, err)
Lines 8–23 set up the problem and line 23 obtains the solution. Lines 25–33 cope
with the visualization. (Note the transposed arguments in line 28, see Section 4.2.2.)
Lines 35–37 construct a minimal error check. The graphical output of this snippet is
Figure 9.1 The numerical solution u(t, x) of the periodic linear advection problem (9.9) for
x ∈ [0, 2π) and t ∈ [0, 64π].
displayed as Figure 9.1. This figure illustrates the virtue of the pseudospectral approach.
Even a relatively coarse grid spacing can produce a smooth result, and the final error
here is about 10−4 . It would require a much finer grid, with the associated work over-
head, to produce a comparable result using purely finite difference techniques.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
9.6 Spectral Techniques for Non-Periodic Problems 199
The vast majority of interesting initial boundary value problems do not exhibit spatial
periodicity, so the Fourier-series-related spectral techniques described above cannot be
applied. An alternative approach might seem to be approximation of functions by means
of polynomials.
For the sake of simplicity, let us map the interval a x b to −1 x 1, e.g., by a
linear transformation. Next we choose a set of N+1 discrete points −1 = x0 < x1 < · · · <
xN = 1. We now try to obtain a useful polynomial approximation to a function u(x). It
is easy to see that there exists a unique polynomial pN (x) of order N which interpolates
u, i.e., pN (xn ) = u(xn ) for all n in [0, N]. Indeed, there exists a pair of NumPy functions
polyfit and poly1d (see Section 4.7) which carry out these calculations. Assuming
uniform convergence, we could differentiate the interpolating polynomial to provide an
estimate of u (x) at the grid points.
Surprisingly, for uniformly spaced grid points, the assumed convergence does not
occur. This is known as the Runge effect, which is exemplified by the following code
snippet, which examines the perfectly smooth function u(x) = 1/(1+25x2 ) for x ∈ [0, 1],
whose values range between 1/26 and 1.
1 import numpy as np
2
5 N=20
6 x_grid=np.linspace(-1.0,1.0,N+1)
7 u_grid=u(x_grid)
8 z=np.polyfit(x_grid,u_grid,N)
9 p=np.poly1d(z)
10 x_fine=np.linspace(-1.0, 1.0, 5*N+1)
11 u_fine=p(x_fine)
12 u_fine
In line 8, the array z is filled with the coefficients of the interpolating polynomial, and
p created in line 9 is a function which returns the interpolating polynomial itself. p(x)
agrees with u(x) at the grid points. However, if we create a finer grid in line 10 and
evaluate p(x) on it, then the values obtained range between −58 and +4. This poly-
nomial is useless for approximating the function! Increasing the value of N does not
help.2
Fortunately, the situation improves markedly if we turn our attention to certain grids
with non-uniform spacing. Suppose we build first a uniform θ-grid on [0, π], and use
the transformation x = − cos θ to construct the non-uniform Chebyshev grid nodes on
2 For continuous but non-differentiable functions, the situation is much worse, e.g., if u(x) = |x|, we obtain
convergence at x = 0, ±1 and at no other points!
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
200 PDEs: A Pseudospectral Approach
[−1, 1]
kπ
θk = , xk = − cos θk , k ∈ [0, N]. (9.10)
N
Let Qk (x) be the Nth-order polynomial in x defined by
(−1)k sin θ sin(Nθ)
Qk (x) = , (9.11)
Nck (cos θk − cos θ)
where ck = 1 for j = 1, 2, . . . , N − 1, while c0 = cN = 2. Then for 0 j, k N it is
straightforward to show that
Qk (x j ) = δ jk . (9.12)
Let f (x) be an absolutely continuous function on [−1, 1] and set fn = f (xn ). It follows
that the Nth-order polynomial which interpolates f (x) at the Chebyshev nodes is
N
fN (x) = fk Qk (x), where fk = f (xk ). (9.13)
k=0
Then it can be shown that, for every absolutely continuous function f (x) on [−1, 1],
the sequence of interpolating polynomials fN (x) converges to f (x) uniformly as N in-
creases. (Note that we do not require f (−1) = f (1).) These are the ones we shall use.
Next we need to consider how to transform between f (x) and the set of { fk }. This is
most economically performed using a specific choice of orthogonal polynomials. That
choice depends on the norms chosen, but if, as here, we use the maximum (C ∞ ) norm,
then the optimal choice is the set of Chebyshev polynomials {T n (x)},
see (9.10), Boyd (2001) and Fornberg (1995). There is another compelling reason for
this choice. In general, the number of operations required to estimate a derivative will be
O(N 2 ). However, it is easy to see that T m (xn ) = cos(mnπ/N). Thus, if we use Chebyshev
polynomials evaluated at Chebyshev nodes, then the transformation from physical to
Chebyshev space, its inverse and the process of differentiation in Chebyshev space can
all be handled using the discrete fast Fourier cosine transform, which takes O(N log N)
operations. Equally importantly, we may expect exponential convergence for smooth
functions.
Besides being a very useful reference for the theoretical ideas we have used in this
section, Fornberg (1995) contains, in its Appendix A, detailed code in Fortran77 for the
practical implementation of these ideas. The philosophy of this book is to try to avoid
programming in compiled languages. How then are we to proceed? Two strategies are:
1. We could try to figure out what these Fortran subroutines and functions do, and
then try to implement them in Python. Unfortunately, only the base FFT function is
available from SciPy, and it uses a different convention.
2. We could try to implement them using the original Fortran77 code, but with a wrap-
per around them which makes them look and behave like Python functions.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
9.7 An Introduction to f2py 201
This second approach is the one we shall use here. Besides solving the immediate cur-
rent problem, we shall see how, in general, one can reuse legacy Fortran code. (This
procedure was already used to generate many of the SciPy functions.) The next two
sections discuss how to wrap pre-existing Fortran code, and the discussion of spectral
methods continues in the one after that.
The f2py tool was originally in SciPy but as it matured it has gravitated to NumPy.
You can check that it is installed, and obtain a summary of its options, by typing in the
command line f2py --verbose. What does it do? Well, originally it was designed to
compile Fortran77 subroutines and place a wrapper around the result so that it could be
used as a Python function. Later that was extended to C functions and more recently to
those parts of Fortran903 which could have been coded in Fortran77. The philosophy of
this book is to use Python wherever possible, so why should f2py be of interest? There
are two major applications which could be useful for the intended readership, who, I
assume, have no knowledge of Fortran.
1. There could be, as here, legacy code, usually in the form of Fortran77 subroutines,
that we would like to reuse, even if our knowledge of Fortran is minimal.
2. When developing a major project, it is advisable to move the time-expensive number-
crunching operations into simple Python functions. An example of arranging a Python
project to facilitate this approach is given in Chapter 10. If the Python profiler shows
that they are too slow, we can then recode the operations as simple Fortran subrou-
tines, compile and wrap them using f2py, and replace the slow Python functions
with the superfast ones.
Unfortunately, f2py documentation for beginners is patchy. The official documenta-
tion4 is somewhat out of date. There are various third-party accounts available on the
internet, none of which is entirely satisfactory. However, for most scientists the tasks
enumerated above can be accomplished using a small number of concepts, which are
illustrated in the following examples.
3 subroutine norm(u,v,w,s)
3 By Fortran90, we mean implicitly also many elements of Fortran95.
4 See http://cens.ioc.ee/projects/f2py2e/.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
202 PDEs: A Pseudospectral Approach
Comments in Fortran90 follow an exclamation mark (!). We need to know that all of
the input arguments and output identifiers have to be supplied as input arguments to a
subroutine, lines 3–7 of the snippet. Line 4 declares that three arguments are real(8)
(roughly a Python float) and are to be regarded as input variables. Line 5 declares s to be
a real, but more importantly it delivers a result which can be accessed from the calling
program.
It is important to supply f2py with syntactically correct Fortran. We check this by
trying to compile the code, using a command line like
gfortran norm3.f90
You should use the name of your compiler if it differs from mine. (Note that one can
actually run this and subsequent command lines from within IPython. Simply start the
line with an exclamation mark (!), as explained in Section 2.4.) If there are syntax
errors, they will be described, after which the compiler should complain about undefined
“symbols”. Make sure that the syntax errors are eliminated. Now try
You should find a file called normv3.so in your current directory.5 Now fire up the
IPython interpreter and type import normv3 followed by normv3?. You will find that
module normv3 contains a function norm, so try normv3.norm?. This function looks
like a Python one which accepts three floats and produces one. Try it out with, e.g.,
normv3.norm(3,4,5)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
9.7 An Introduction to f2py 203
3 SUBROUTINE NORM(U,V,W,S)
4 REAL*8 U,V,W,S
5 S=SQRT(U*U+V*V+W*W)
6 END
Next delete normv3.so and feed this file to f2py via the line6
f2py -c --fcompiler=gfortran norm3.f --f77flags=-O3 -m normv3
and within IPython import normv3 and inspect the new file via normv3.norm?. We
have a problem! The function normv3.norm expects four input arguments. A moment’s
thought reveals the problem. There is no way in Fortran77 to specify “out” variables.
Here, though, f2py has the solution. Amend the code above to
1 C FILE NORM3.F A SIMPLE SUBROUTINE IN F77
2
3 SUBROUTINE NORM(U,V,W,S)
4 REAL*8 U,V,W,S
5 Cf2py intent(in) U,V,W
6 Cf2py intent(out) S
7 S=SQRT(U*U+V*V+W*W)
8 END
We have inserted Fortran90-like directives into Fortran77 code via comment statements.
Now delete the old normv3.so file and create a new one. The resulting normv3.norm
function has the expected form and behaves identically to its Fortran90 predecessor.
3 SUBROUTINE NORM(U,S,N)
4 INTEGER N
5 REAL*8 U(N)
6 REAL*8 S
7 Cf2py intent(in) N
6 The shortened command line versions given above can also be used here.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
204 PDEs: A Pseudospectral Approach
8 Cf2py intent(in) U
9 Cf2py depend(U) N
10 Cf2py intent(out) S
11 REAL*8 SUM
12 INTEGER I
13 SUM = 0.0D0
14 DO 100 I=1,N
15 100 SUM = SUM + U(I)*U(I)
16 S = SQRT(SUM)
17 END
Ignore for the moment lines 7–10. Unlike Python, Fortran arrays do not know their size,
so it is mandatory to supply both U and N as arguments, and we need of course S, the
output argument. Lines 11–16 carry out the calculation. The construction in lines 14
and 15 is a do-loop, the Fortran equivalent of a for-loop. The subroutine arguments look
strange to a Python user, but we can remedy this. Lines 7, 8 and 10 merely declare the
input and output variables as before. Line 9 is new. It tells f2py that N and U are related
and that the value of N can be inferred from that for U. We now create a new module
with, e.g.,
f2py -c --fcompiler=gfortran normn.f --f77flags=-O3 -m normn
and within IPython import normn and inspect the function normn.normn. We find that
it requires u as an input argument, and n is an optional input argument, while s is the
output argument. Thus
normn.norm([3,4,5,6,7])
works just like a Python function. You should try also normn.norm([3,4,5,6,7],3).
The adventurous reader might like to test normn.norm([3,4,5,6,7],6).
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
9.7 An Introduction to f2py 205
the number of copies, although it is hard to avoid at least one copy operation. Here are
two very simple ways to minimize copying.
1. If the Fortran is expecting a real*8 array, make sure that Python supplies a NumPy
array of float. (In the example above, we supplied a list of integers, which certainly
causes copying!) If you are using the np.array function, see Section 4.2, you can
actually specify order=’F’ to get round the problem.
2. Although it is a common Fortran idiom to input, modify and output the same large
array in a single subroutine, this often leads to programming errors, and also to
copying in f2py. A more explicit Pythonic style, which leads to a single copy, is to
be recommended.
Both of these ideas are illustrated in the following snippet and IPython session.
1 C FILE: MODMAT.F MODIFYING A MULTIDIMENSIONIAL ARRAY
2
3 SUBROUTINE MMAT(A,B,C,R,M,N)
4 INTEGER M,N
5 REAL*8 A(M,N),B(M),C(N),R(M,N)
6 Cf2py intent(in) M,N
7 Cf2py intent(in) A,B,C
8 Cf2py depend(A) M,N
9 Cf2py intent(out) R
10 INTEGER I,J
11 DO 10 I=1,M
12 DO 10 J=1,N
13 10 R(I,J)=A(I,J)
14 DO 20 I=1,M
15 20 R(I,1)=R(I,1)+B(I)
16 DO 30 J=1,N
17 30 R(1,J)=R(1,J)+C(J)
18 END
In words, array R is a copy of A with the first column increased by adding B and the first
row increased by adding C. Suppose this snippet is compiled to a module modmat using
f2py. Then consider the following IPython session
import numpy as np
import modmat
a = np.array([[1,2,3],[4,5,6]],dtype=’float’,order=’F’)
b=np.array([10,20],dtype=’float’)
c=np.array([7,11,13],dtype=’float’)
r=modmat.mmat(a,b,c)
r
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
206 PDEs: A Pseudospectral Approach
The examples presented in the previous section are of course over-simplified. (The
author had to strike a balance between informing and confusing the reader.) In this
section, we look at a more realistic task of turning legacy Fortran code into a useful
suite of Python functions, presuming only a minimal knowledge of Fortran. Clearly,
only one such task can be incorporated into this book, so it is unlikely that the reader’s
favourite application will be covered. Nevertheless, it is very worthwhile to follow the
process carefully, so as to see how to apply the same procedure to other applications.
Our example is the task set aside in Section 9.6, namely to implement the Chebyshev
transform tools described in Fornberg (1995) and given in legacy Fortran code there.
We start by typing or copying the first subroutine from the code in Appendix B to a
file, say cheb.f. Actually, this is not given entirely in Fornberg (1995). The working
lines 8–10 are given as a snippet at the start of Section F.3 on page 187 of that reference,
and the earlier lines 1–4 and later lines 11 and 12 are confected using the code examples
given earlier on that page. As the comment line 2 makes clear, N is an input argument, X
is an output one, and it refers to a vector of dimension N+1. We need to help f2py with
the comment lines 5–7.
We move next to the second code snippet, the basic fast discrete Fourier transform
SUBROUTINE FFT which runs (before enhancements) to 63 lines of rather opaque code.
Comment lines 10–12 reveal a common Fortran idiom. The arrays A and B are input
variables which will be altered in situ and then become output variables. Fortunately,
f2py is equipped to handle this, and line 21 deals with the problem. As can be seen
in lines 14–17, the integer arguments IS and ID are input variables, so line 22 reminds
f2py of this. Line 16 informs us that N is the actual dimension of the arrays A and B. As
in the previous section, we could handle this with a line
Cf2py depend(A) N
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
9.8 A Real-Life f2py Example 207
1 import numpy as np
2 import cheb
3 print cheb.__doc__
4
5 for N in [8,16,32,64]:
6 x= cheb.chebpts(N)
7 ex=np.exp(x)
8 u=np.sin(ex)
9 dudx=ex*np.cos(ex)
10
11 uc=cheb.tocheb(u,x)
12 duc=cheb.diffcheb(uc)
13 du=cheb.fromcheb(duc,x)
14 err=np.max(np.abs(du-dudx))
15 print "with N = %d error is %e" % (N, err)
The error is O(10−3 ) with N = 8, dropping to O(10−8 ) for N = 16 and O(10−14 ) for
N = 32.
This completes our construction of a suite of functions for carrying out spatial dif-
ferentiation in Chebyshev space. The same principles could be used to extend Python
in other directions. We can wrap Fortran90 or Fortran95 code in exactly the same way,
provided we remember that comments start (not necessarily at the beginning of the line)
with an exclamation mark (!) rather than a C. Well-written Fortran90 code will include
specifications like intent(in), but there is no harm in including the f2py version,
provided both are consistent with each other.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
208 PDEs: A Pseudospectral Approach
ut = −u u x + μ u xx , (9.15)
where μ is a positive constant parameter. Here the time development of u(t, x) is con-
trolled by two effects, the non-linear advection represented by the first term on the right-
hand side of (9.15), and the linear diffusion represented by the second term. A number
of exact solutions are known, and we shall use the kink solutions
' (
c
#u(t, x) = c 1 + tanh (ct − x) , (9.16)
2μ
where c is a positive constant, to test our numerical approach. (For fixed t and large
negative x, u ≈ 2c, while if x is large and positive, u ≈ 0. There is a time-dependent
transition between two uniform states, hence the name kink.)
We shall restrict consideration to the intervals −2 t 2 and −1 x 1. We shall
need to impose initial data at t = −2, so we set
' (
c
u(−2, x) = f (x) = c 1 − tanh (x + 2c) , (9.17)
2μ
B1 [t, u] ≡ u2 (t, −1) − μu x (t, −1) − g1 (t) = 0, B2 [t, u] ≡ μu x (t, 1) − g2 (t) = 0, (9.18)
for −2 t 2. Here g1 (t) and g2 (t) are arbitrary functions. (For the sake of definiteness,
we shall later choose them so that the conditions (9.18) are exactly satisfied by the
exact solution (9.16).) Implementing the finite difference method to deal with boundary
conditions such as (9.18) is dealt with in the standard textbooks. Subject to stability
and convergence requirements, discrete changes in u(t, x) at the boundary grid points
can easily be accommodated in a finite difference scheme. However, much more care
is needed in a pseudospectral scheme in order to avoid the occurrence of the Gibbs
phenomenon due to a perceived discontinuity.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
9.9 Worked Example: Burgers’ Equation 209
subject only to the initial conditions (9.17). Here τi are constants, and S i (x) are suitable
functions of x. It appears that we solve neither Burgers’ equation nor the boundary
conditions, but a combination of the two. The basic idea is that, if the τi are chosen
large enough, then any deviations of u(t, x) from a solution of the boundary conditions
(9.19) will be reduced to zero exponentially quickly. Indeed, the τi → ∞ limit of (9.19)
is the boundary condition (9.18). But a solution of (9.19) which satisfies the boundary
conditions also satisfies the evolution equation (9.15). This idea is not new, and neither
is it restricted to boundary conditions. For example, it appears in numerical general
relativity, where the evolution-in-time equations are subject to elliptic (constant-time)
constraints as “constraint damping”.
The penalty method applied to boundary conditions takes a surprisingly simple form
in the pseudospectral approach being advocated here. As pointed out first by Funaro
and Gottlieb (1988) and later by Hesthaven (2000), it is advantageous to choose S 1 (x)
in (9.19) to be the Q0 (x) defined by (9.11), and similarly to replace S 2 (x) by QN (x). Re-
calling equation (9.12), we see that insofar as the grid approximation is concerned, we
are imposing the left (right) boundary condition at the leftmost (rightmost) grid point,
and the interior grid points do not “see” the boundary conditions. Detailed calculations
for linear problems suggest that the method will be stable and accurate if the parameters
τi are large, O(N 2 ).
Making due allowance for the more intricate equation and the need to impose bound-
ary conditions, the code snippet given below for solving Burgers’ equation for the kink
solution is hardly more complicated than that used to solve the linear advection equa-
tion with periodic boundary conditions in Section 9.5. Note that the snippet computes
the numerical solution, displays it graphically and estimates the error.
1 import numpy as np
2 from scipy.integrate import odeint
3 import matplotlib.pyplot as plt
4 from mpl_toolkits.mplot3d import Axes3D
5 %matplotlib notebook
6 import cheb
7
8 c=1.0
9 mu=0.1
10
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
210 PDEs: A Pseudospectral Approach
Figure 9.2 The numerical evolution of a “kink” solution (9.16) of the Burgers equation (9.15)
treated as an initial boundary value problem.
11 N=64
12 x=cheb.chebpts(N)
13 tau1=N**2
14 tau2=N**2
15 t_initial=-2.0
16 t_final=2.0
17
18 def u_exact(t,x):
19 """ Exact kink solution of Burgers’ equation. """
20 return c*(1.0+np.tanh(c*(c*t-x)/(2*mu)))
21
22 def mu_ux_exact(t,x):
23 """ Return mu*du/dx for exact solution. """
24 arg=np.tanh(c*(c*t-x)/(2*mu))
25 return 0.5*c*c*(arg*arg - 1.0)
26
27 def f(x):
28 """ Return initial data. """
29 return u_exact(t_initial,x)
30
31 def g1(t):
32 """ Return function needed at left boundary. """
33 return (u_exact(t,-1.0))**2-mu_ux_exact(t,-1.0)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
9.9 Worked Example: Burgers’ Equation 211
34
35 def g2(t):
36 """ Return function needed at right boundary. """
37 return mu_ux_exact(t,1.0)
38
51 t=np.linspace(t_initial, t_final,81)
52 u_initial=f(x)
53 sol=odeint(rhs,u_initial,t,rtol=10e-12,atol=1.0e-12,mxstep=5000)
54 xg,tg=np.meshgrid(x,t)
55 ueg=u_exact(tg,xg)
56 err=sol-ueg
57 print "With %d points error is %e" % (N,np.max(np.abs(err)))
58
59 plt.ion()
60 fig=plt.figure()
61 ax=Axes3D(fig)
62 ax.plot_surface(xg,tg,sol,rstride=1,cstride=2,alpha=0.1)
63 ax.set_xlabel(’x’,style=’italic’)
64 ax.set_ylabel(’t’,style=’italic’)
65 ax.set_zlabel(’u’,style=’italic’)
Table 9.2 The maximum absolute error as a function of the Chebyshev grid size N . The
relative sizes are of interest here.
Lines 7 and 8 set up the parameters for the exact solution, and lines 10–15 set up
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
212 PDEs: A Pseudospectral Approach
the grid and ad hoc values for τi . Line 45 sets up the right-hand side of (9.19) at the
interior points and lines 46 and 47 add in the penalty terms at the end points. The rest
of the code should be a variant of earlier code snippets. The picture generated by this
snippet is shown in Figure 9.2. The maximum absolute error computed by the snippet
for different choices of N is shown in Table 9.2. Notice that doubling the number of
grid points squares the error, but in the transitions from N = 64 to N = 256 both the
numerical accuracy errors discussed earlier in Section 9.4 and the errors generated by
the odeint evolution function are starting to intrude, spoiling the picture. The evolution
errors can be mitigated by changing the accuracy parameter, but the others are inherent
in the method.
This completes the survey of spectral methods coupled to the method of lines for solv-
ing parabolic problems, or hyperbolic ones where the solution is known a priori to be
smooth. Non-linearities and complicated boundary conditions have been covered, and
the principal omission is a discussion of systems of equations. However, the philosophy
behind the method-of-lines approach is to reduce the time integration of an evolution-
ary partial differential to solving a set of ordinary differential equations. Indeed, both
code snippets used the SciPy odeint function. When we treated ordinary differential
equations in Chapter 8, we showed there how to integrate systems of equations, and all
that is required to deal with omission here is to merge those techniques into the snippets
given here.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.011
10 Case Study: Multigrid
In this final chapter, we present an extended example or “case study” of a topic which is
relevant to almost all of the theoretical sciences, called multigrid. For many, multigrid
is a closed and forbidding book, so we first look at the type of problems it can be
used to solve, and then outline how it works, finally describing broadly how it can be
implemented very easily in Python. The rest of the chapter fleshes out the details.
In very many problems, we associate data with points on a spatial grid.1 For simplic-
ity, we assume that the grid is uniform. In a realistic case, we might want a resolution of,
say, 100 points per dimension, and for a three-dimensional grid we would have 106 grid
points. Even if we store only one piece of data per grid point, this is a lot of data which
we can pack into a vector (one-dimensional array) u of dimension N = O(106 ). These
data are not free but will be restricted either by algebraic or differential equations. Using
finite-difference (or finite-element) approximations, we can ensure that we are dealing
with algebraic equations. Even if the underlying equations are non-linear, we have to
linearize them (using, e.g., a Newton–Raphson procedure, see Section 10.3) for there is
no hope of solving such a large set of non-linear equations. Thus we are faced with the
solution of a very large linear system of equations of the form
Au = f, (10.1)
where f is an N-dimensional vector and A is an N × N matrix.
The direct solution of the system (10.1), e.g., by matrix inversion, is usually a non-
starter. Unless the matrix A has a very special form, its inversion involves O(N 3 ) oper-
ations! Instead, we have to use iterative methods (discussed in Section 10.2.1). These
aim, by iterating on u, to reduce the residual r = f−Au to zero. However, these methods,
when used in isolation, all suffer from a fatal flaw. To explain this, we need to consider
a Fourier representation of the residual r, which will consist of O(N) modes. (This
is discussed in more detail in Section 10.1.2.) The iterative methods will reduce very
rapidly the magnitudes of about one half of the Fourier modes (the higher-frequency
ones) of the residual to near zero, but will have negligible effect on the other half, the
lower-frequency ones. After that, the iteration stalls.
At this point, the first key idea behind multigrid emerges. Suppose we consider an
additional grid of linear dimension N/2, i.e., double the spacing. Suppose too that we
carefully transcribe the stalled solution u, the source f and the operator A from the orig-
inal fine grid to the new coarse grid. This makes sense because the fine grid residual
1 In the finite element approach, the vocabulary is different, but the ideas are the same.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
214 Case Study: Multigrid
was made up predominantly of low-frequency modes, which will all be accurately rep-
resented on the coarse grid. However, half of these will be of high frequency, as seen
in the coarse grid. By iteration, we can reduce them nearly to zero. If we then carefully
transfer the solution data back to the fine grid, we will have removed a further quarter
of the Fourier modes of the residual, making three quarters in total.
The second key idea is to realize that we are not restricted to the two-grid model. We
could build a third, even-coarser, grid and repeat the process so as to eliminate seven-
eighths of the residual Fourier modes. Indeed, we could carry on adding coarser grids
until we reach the trivial one. Note too that the additional grids take up in total only
a fraction of the original grid size, and this penalty is more than justified by the speed
enhancement.
It is important to note that the operations on and information transfers between grids
are, apart from the sizes, the same for all grids. This suggests strongly the use of a
Python class structure. Further, once the two-grid model has been constructed, the rest
of the procedure can be defined very simply using recursion. In the rest of this chapter,
we fill out some of the details, and set up a suite of Python functions to implement them,
ending with a non-trivial non-linear example.
Although we have tried to make this chapter reasonably self-contained, interested
readers will want to explore further. Arguably the best place for beginners to start is
Briggs et al. (2000), although for the first half of their book there are a number of
web-based competitors. However, in the second half Briggs et al. (2000) sketch in a
clear and concise way a number of applications. The book by Trottenberg et al. (2001)
covers much the same material in a more discursive manner, which some may find
more helpful. The textbook by Wesseling (1992) is often overlooked, but its algorithmic
approach, especially in Chapter 8, is not matched elsewhere. The monograph by Brandt
and Livne (2011) contains a wealth of useful information.
We need a concrete example, which will act as a paradigm for a few fundamental ideas.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
10.2 The Tools of Multigrid 215
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
216 Case Study: Multigrid
components of a new approximation ũ(1) . Suppose we write this process as ũ(1) = J(ũ(0) ).
Under reasonable conditions, we can show that the kth iterate ũ(k) = J k (ũ(0) ) converges
to the exact solution u as k → ∞. In practice, we make a small modification. Let ω be a
parameter with 0 < ω 1, and set
u = ũ(k) + e(k) ,
where e(k) is the error. It is straightforward to show, using the linearity, that e(k) also
satisfies (10.4) but with zero source term f .
We now illustrate this by a concrete example. We set n = 16 and choose as initial
iterate for the error mode
e(0) = s1 + 13 s13 ,
where the sk are the discrete Fourier modes defined in (10.5) and (10.6). This is the
superposition of the fundamental mode, and one-third of a rapidly oscillating mode.
Figure 10.1 shows this, and the results of the first eight Jacobi iterations. It was produced
Figure 10.1 Jacobi iteration on a grid with 16 intervals. The initial iterate of the error is a
mixture of the first and thirteenth harmonics, the extremely jagged curve. The results of eight
weighted Jacobi iterations are shown. Iteration damps the rough (thirteenth) mode very
effectively but is extremely slow at reducing the smooth fundamental mode in the error.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
10.2 The Tools of Multigrid 217
5 N=16
6 omega=2.0/3.0
7 max_its=8
8
9 x=np.linspace(0,1,N+1)
10 s1=np.sin(np.pi*x)
11 s13=np.sin(13*np.pi*x)
12 e_old=s1+s13/3.0
13 e=np.zeros_like(e_old)
14
15 plt.ion()
16 plt.plot(e_old)
17 for it in range(max_its):
18 e[1:-1]=(1.0-omega)*e_old[1:-1]+\
19 0.5*omega*(e_old[0:-2]+e_old[2: ])
20 plt.plot(e)
21 e_old=e.copy()
22 plt.xlabel(’The 17 grid points’)
23 plt.ylabel(’The first 8 iterates of the error’)
Here our initial iterate was the superposition of the fundamental mode and one-third
of a rapidly oscillating mode, the very spiky curve in Figure 10.1. The remaining curves
show the results of the first eight Jacobi iterations. After three iterations, the amplitude
of the spikes has all but vanished, but the reduction of the now smooth error e(m) to the
exact value zero is extremely slow. You might like to alter the code snippet to verify that
the same result holds for initial data made up of other mixtures of smooth and rough
modes. The rough modes are damped rapidly, the smooth ones hardly at all. This is the
heuristic reason for the name relaxation.
It is easy to see the theoretical foundation for these results. Note first that, using a
standard trigonometrical identity, sk defined by (10.6) is an eigenvector of the operator
J with eigenvalue cos(kπ/n). It follows that sk is an eigenvector of operator W defined
by (10.9), with eigenvalue
λk = 1 − ω + ω cos(kπ/n) = 1 − 2ω sin2 ( 12 kπ/n). (10.10)
The damping of the kth Fourier mode is determined by |λk |. If k n, we see that
λk ≈ 1 − O(1/n2 ). Thus damping of smooth modes is negligible, especially for large
n. Thus although relaxation is convergent onto the exact solution, the ultraslow conver-
gence of smooth modes renders it useless. Note, however, that if we consider the rough
modes, n/2 k < n, and we choose ω = 2/3, then |λk | < 1/3. The weighted Jacobi
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
218 Case Study: Multigrid
iteration then becomes a very efficient damper of rough modes. In multigrid terms, it is
a smoother.
There are of course many other classical relaxation methods, the most common being
Gauss–Seidel. The Gauss–Seidel method utilizes components of u(k+1) on the right-
hand side of (10.8) as soon as they have been computed. Thus it is really a family
of methods depending on the order in which the components are computed. Here, for
example, is a single red–black Gauss–Seidel iteration applied to the vector ũ(k)
) 1 (k) (k)
2 (ũi−1 + ũi+1 + Δx fi ) i = 2, 4, . . . , n − 2,
2
ũ(k+1) = 1 (k+1) (k+1) (10.11)
i
(ũ
2 i−1 + ũ i+1 + Δx 2
fi ) i = 1, 3, . . . , n − 1.
Here points with i even are “red”, while those with i odd are called “black”. This extends
to two dimensions, where points with i and j both even or both odd are “red”, while the
remaining points are “black”. The similarity to a chess board gives rise to the name.
The behaviour of Gauss–Seidel methods is similar to that of weighted Jacobi iteration.
They usually damp the rough modes a little faster, but suffer from the same defect when
treating the smooth ones. Thus none of them taken in isolation can be used to solve the
original problem (10.4).
Fortunately, multigrid offers a very elegant solution. The central tenet of multigrid is
not to consider the problem on a single fixed grid, n = 16 above, but instead to look at it
on a succession of grids, e.g., n = 16, 8, 4, 2. Scaling by factors other than 2 is possible,
but this is the usual choice. Henceforth, we shall assume that the finest grid size n is a
power of 2.
Au = f,
on a “finest grid” of size n with u0 and un specified. Suppose we have some approxima-
tion ũ to u. We define the error as e = u − ũ, and the residual as r = f − Aũ. Then it is
easy to verify that the residual equation
Ae = r, e0 = en = 0, (10.12)
holds. It is apparently trite but nonetheless important to recognize that systems (10.4)
and (10.12) are formally identical.
We recall also that our finest grid is a partition of 0 x 1 by n intervals. Setting
h = Δx = 1/n, we have set xi = ih, u(xi ) = ui etc. for 0 i n. Also n is sufficiently
large that u and f are smooth on the finest grid, and n is a power of 2. For the mo-
ment, we call the finest grid the fine grid. We consider also a coarse grid with spacing
H = 2h, i.e., there are n/2 intervals, which is intended to be a replica of the fine grid. It
is convenient to distinguish the fine grid quantities denoted by, e.g., uh from the corre-
sponding coarse grid ones, e.g., uH . How are they related? Well, for the linear problems
under consideration we can require AH to be obtained from Ah under the transformation
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
10.2 The Tools of Multigrid 219
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
220 Case Study: Multigrid
would be minimal. Therefore, a good working rule is to smooth before restricting. Thus
we have a “golden rule”: smooth after prolongation but before restriction.
Before we introduce specific schemes, we shall widen the discussion to consider a more
general problem
L(u(x)) = f(x), (10.16)
on some domain Ω ⊆ Rn . Here L is a, possibly non-linear, elliptic operator, and u(x) and
f(x) are smooth vector-valued functions defined on Ω. In order to keep the discussion
simple, we shall assume homogeneous Dirichlet boundary conditions,
on the boundary ∂Ω of the domain Ω. More general boundary conditions are treated in
the literature already cited.
Suppose ũ(x) is some approximation to the exact solution u(x) of (10.16). As before,
we define the error e(x) and residual r(x) by
which follows from (10.16) and (10.18). Note that, if the operator L is linear, then the
left-hand side of (10.19) simplifies to L(e), so there is no inconsistency with the earlier
definition (10.12).
A further pair of remarks is appropriate at this stage. We assume that we have con-
structed a fine grid Ωh with spacing h which covers Ω. Using the previous notation, we
aim to solve
Lh (uh ) = f h on Ωh , uh = 0 on ∂Ωh , (10.20)
rather than the continuous problem (10.16) and (10.17). The important point to note
is that it is not necessary to solve the grid equation (10.20) exactly. For, even if we
found the exact solution, uh,exact , this would not furnish an exact solution uexact for the
continuous problem. Instead, we might expect to obtain
for an appropriate norm and some bound h which tends to zero as h → 0. Thus it
suffices to obtain an approximate grid solution ũh with the property
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
10.3 Multigrid Schemes 221
The second remark concerns what we mean by “smoothing” in this more general non-
linear context. Suppose we label the grid points in the interior of Ωh by i = 1, 2, . . . , n−1
in some particular order. Then (10.20) becomes
Lih (uh1 , uh2 , . . . , uhi−1 , uhi , uhi+1 , . . . , uhn−1 ) = fih , 1 i n − 1.
The Gauss–Seidel (GS) iteration process solves these equations in sequence using new
values as soon as they are computed. Thus
Lih (uh,new
1 , uh,new
2 , . . . , uh,new
i−1 , ui , ui+1 , . . . , un−1
h,new h,old h,old
) = fih
generates a single scalar equation for the unknown uh,new i . If this is non-linear, then we
may use one or more Newton–Raphson (NR) iterations to solve it approximately, e.g.,
' (
Li (u) − fi
h,new
ui = ui −
h,old
. (10.23)
∂Li (u)/∂ui (uh,new ,uh,new ,...,uh,new ,uh,new ,uh,old ,...,uh,old )
1 2 i−1 i i+1 n−1
The NR iterations should converge quadratically if the iterate is within the domain of
attraction, and usually one iteration suffices.
In practice, this GS–NR process acts as an effective smoother, just as GS does in the
linear case.
Figure 10.2 The two-grid scheme. The upper level corresponds to a grid with spacing h while the
lower one refers to a grid with spacing H where H > h. Time is flowing from left to right. The
scheme should produce a more accurate solution on the finer grid. It may be iterated many times.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
222 Case Study: Multigrid
for eH . How this is to be done will be explained later. Notice that, setting uH =
ũH + eH , the residual equation can be written as
LH (uH ) = Ih H f h + τH , (10.27)
and the linearity of IhH has been used. The tau-correction is a useful measure of
the errors inherent in the grid transfer process.
Correct step Prolong eH back to the fine grid. Since ũH + eH is presumably smooth on
the coarse grid, this is an unambiguous operation. Then one should be able to
improve ũh by the step ũh → ũh + eh .
Smooth step Finally, we apply a small number ν2 of relaxation iteration steps to ũh .
Figure 10.2 illustrates this process. Each horizontal level refers to a particular grid spac-
ing, and lower level(s) refer to coarser grid(s). As we progress from left to right at the
top level, we hope to obtain a better approximation to the solution of the initial prob-
lem, although we may need to iterate the scheme several times. There are no convincing
theoretical arguments for establishing values for the parameters ν1 and ν2 . They must
be chosen empirically.
This two-grid algorithm is often referred to as the full approximation scheme (FAS).
It is designed to handle non-linear problems and so has to implement the non-linear
version of the residual equation (10.19) which requires the transfer of an approximate
solution from the fine to the coarse grid. In the linear case, it is necessary only to transfer
the error, see, e.g., (10.12).
As presented the two-grid algorithm suffers from two very serious defects. The first
appears in the initial approximation step. How is the initial choice of ũh to be made?
In the linear case, there is no issue, for we might as well choose ũh = 0. But in the
non-linear case, this choice may not lie in the domain of attraction for the Newton–
Raphson iteration, which would upset the smoothing step. In Section 10.3.3, we remove
this obstacle. The second defect is how are we to generate the solution of the residual
equation in the solution step? We deal with this in the next section.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
10.3 Multigrid Schemes 223
Figure 10.3 The V-cycle scheme. The upper level corresponds to a grid with spacing h while the
lower ones refer to progressively coarser grids. Time is flowing from left to right. The scheme
should produce a more accurate solution on the finer grid. It may be iterated many times, hence
the name “cycle”. Notice how the “golden rule”, smooth before coarsening and after refining, is
applied automatically.
adding ever coarser grids until we reach one with precisely one interior point, where
we construct an “exact” solution on this grid. Notice that each V-cycle comes with the
two parameters ν1 and ν2 inherited from the two-grid algorithm. It should be apparent
from Figure 10.3 that we could iterate on V-cycles, and in practice this is usually done
ν0 times.
Figure 10.4 The FMG scheme. The circular dots correspond to the final solution and the square
dots indicate the initial approximate solution for one or more V-cycles at the given level, while
the dotted lines correspond (as before) to an interpolation step. As time progresses from left to
right, the transition from the initial to the final solution occurs via a sequence of V-cycles.
We turn now to the initialization problem for the two-grid algorithm. For linear prob-
lems, we can always start from the trivial solution, but this is not possible, usually, for
non-linear ones. The V-cycle presented above suggests an obvious solution. Suppose
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
224 Case Study: Multigrid
we jump to the coarsest grid, solve the problem there and prolong the solution back to
the finer grids to act as a first iterate. In this simple form, the idea doesn’t work. In-
terpolation leads to two types of errors. High-frequency errors may be invisible on the
coarse grid, but should be reduced by smoothing. Aliasing errors, i.e., smooth errors
introduced by high-frequency effects because on the coarse grid they are mistaken for
smooth data, can also occur. However the full multigrid scheme (FMG) gets round this
problem in a very ingenious way. First we solve the problem on the coarsest grid. Then
interpolate the solution to the next-coarsest grid. Now perform ν0 V-cycles on this grid
(which turns out to be a two-grid scheme). Next interpolate the solution to the third-
coarsest grid, perform V-cycles and so on. The scheme is depicted in Figure 10.4. Note
that the interpolation steps (the dotted lines in the figure) are not part of the V-cycles.
While it may be convenient to use the standard prolongation operator for these steps,
there are strong arguments, see, e.g., Brandt and Livne (2011), for using a more accurate
scheme.
The aim of this chapter is to produce a simple Python implementation of multigrid using
the components we have just outlined. Clearly, V-cycles and the FMG are implemented
most elegantly using recursion. Most early implementations used Fortran which then did
not include recursion and so lengthy but reasonably efficient kludges were developed to
circumvent this. Also it should be clear that the levels or grids have a clear structure,
which can be used to clarify the code. Until recently, Fortran lacked the means to im-
plement such structures. Thus most existing codes are very difficult to understand. We
could of course replicate them in Python using the f2py tool from Chapter 9, but this
is not an approach that we would wish to encourage. We shall instead produce a Python
implementation which is clear, concise and simple.
The most efficient way to undertake programming a complex task like multigrid is
to split it into groups of subtasks, and we can identify three obvious groups. The first
is those tasks which depend only on the number of dimensions and not on the actual
problem. Prolongation and restriction fall naturally into this category. Tasks in the sec-
ond group depend on the number of dimensions and the specific problem. The obvious
examples are smoothing and the computation of residuals. The final group consists of
tasks specific to multigrid. Our implementation of this final group will make assump-
tions neither about the number of dimensions nor concerning the details of the specific
problem being studied.
Experience shows that tasks in the first two groups are the computationally expensive
ones. The code used here is a reasonably efficient NumPy one. However, it would be
very easy, using, e.g., f2py, to implement them using a compiled language such as
Fortran with a Python wrapper, because the algorithms used involve just simple loops.
However, the situation is reversed for the third group. The clearest formal definition
of multigrid is recursive in nature. Recursion is fully implemented in Python, but only
partially so in more recent dialects of Fortran. The concept of grids with different levels
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
10.4 A Simple Python Multigrid Implementation 225
of refinement (the vertical spacing in the figures above) is easily implemented using the
Python class construct, which is much simpler to implement than its C++ counterpart.2
For the sake of brevity, we have eliminated all extraneous features and have reduced
comments to the absolute minimum. The scripts presented here should be thought of as
a framework for a more user-friendly suite of programs.
3 import numpy as np
4
5 def l2_norm(a,h):
6 return h*np.sqrt(np.sum(a**2))
7
8 def prolong_lin(a):
9 pshape=(2*np.shape(a)[0]-1,2*np.shape(a)[1]-1)
10 p=np.empty(pshape,float)
11 p[0: :2,0: :2]=a[0: ,0: ]
12 p[1:-1:2,0: :2]=0.5*(a[0:-1,0: ]+a[1: ,0: ])
13 p[0: :2,1:-1:2]=0.5*(a[0: ,0:-1]+a[0: ,1: ])
14 p[1:-1:2,1:-1:2]=0.25*(a[0:-1,0:-1]+a[1: ,0:-1]+
15 a[0:-1,1: ]+a[1: ,1: ])
16 return p
17
18 def restrict_hw(a):
19 rshape=(np.shape(a)[0]/2+1,np.shape(a)[1]/2+1)
20 r=np.empty(rshape,float)
21 r[1:-1,1:-1]=0.5*a[2:-1:2,2:-1:2]+ \
22 0.125*(a[2:-1:2,1:-2:2]+a[2:-1:2,3: :2]+
23 a[1:-2:2,2:-1:2]+a[3: :2,2:-1:2])
24 r[0,0: ]=a[0,0: :2]
25 r[-1,0: ]=a[-1,0: :2]
26 r[0: ,0]=a[0: :2,0]
27 r[0: ,-1]=a[0: :2,-1]
28 return r
29
2 Much of the complexity in C++ classes comes from the security requirement; a normal class user should
not be able to see the implementation details for the class algorithms. This feature is irrelevant for most
scientific users and is not offered in Python.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
226 Case Study: Multigrid
30 #----------------------------------------------
31 if __name__==’__main__’:
32 a=np.linspace(1,81,81)
33 b=a.reshape(9,9)
34 c=restrict_hw(b)
35 d=prolong_lin(c)
36 print "original grid\n",b
37 print "with spacing 1 its norm is ",l2_norm(b,1)
38 print "\n restricted grid\n",c
39 print "\n prolonged restricted grid\n",d
The function in lines 5 and 6 returns an approximation to the L2 -norm of any array a
with spacing h. Lines 8–16 implement prolongation using linear interpolation for a two-
dimensional array a, using the obvious generalization of the one-dimensional version
(10.13). Next lines 18–23 show how to implement restriction for the interior points by
half weighting, which is the simplest generalization of full weighting defined in one
dimension by (10.15) in a two-dimensional context. Finally, lines 24–27 implement a
simple copy for the boundary points. The supplementary test suite, lines 31–39, offers
some simple partial checks on these three functions.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
10.4 A Simple Python Multigrid Implementation 227
3 import numpy as np
4
5 def get_lhs(u,h2):
6 w=np.zeros_like(u)
7 w[1:-1,1:-1]=(u[0:-2,1:-1]+u[2: ,1:-1]+
8 u[1:-1,0:-2]+u[1:-1,2: ]-
9 4*u[1:-1,1:-1])/h2+u[1:-1,1:-1]*u[1:-1,1:-1]
10 return w
11
12 def gs_rb_step(v,f,h2):
13 u=v.copy()
14 res=np.empty_like(v)
15
16 res[1:-1:2,1:-1:2]=(u[0:-2:2,1:-1:2]+u[2: :2,1:-1:2]+
17 u[1:-1:2,0:-2:2]+u[1:-1:2,2: :2]-
18 4*u[1:-1:2,1:-1:2])/h2 +\
19 u[1:-1:2,1:-1:2]**2-f[1:-1:2,1:-1:2]
20 u[1:-1:2, 1:-1:2]-=res[1:-1:2,1:-1:2]/(
21 -4.0/h2+2*u[1:-1:2,1:-1:2])
22
23 res[2:-2:2,2:-2:2]=(u[1:-3:2,2:-2:2]+u[3:-1:2,2:-2:2]+
24 u[2:-2:2,1:-3:2]+u[2:-2:2,3:-1:2]-
25 4*u[2:-2:2,2:-2:2])/h2+\
26 u[2:-2:2,2:-2:2]**2-f[2:-2:2,2:-2:2]
27 u[2:-2:2,2:-2:2]-=res[2:-2:2,2:-2:2]/(
28 -4.0/h2+2*u[2:-2:2,2:-2:2])
29
30 res[2:-2:2,1:-1:2]=(u[1:-3:2,1:-1:2]+u[3:-1:2,1:-1:2]+
31 u[2:-2:2,0:-2:2]+u[2:-2:2,2: :2]-
32 4*u[2:-2:2,1:-1:2])/h2 +\
33 u[2:-2:2,1:-1:2]**2-f[2:-2:2,1:-1:2]
34 u[2:-2:2,1:-1:2]-=res[2:-2:2,1:-1:2]/(
35 -4.0/h2+2*u[2:-2:2,1:-1:2])
36
37 res[1:-1:2,2:-2:2]=(u[0:-2:2,2:-2:2]+u[2: :2,2:-2:2]+
38 u[1:-1:2,1:-3:2]+u[1:-1:2,3:-1:2]-
39 4*u[1:-1:2,2:-2:2])/h2+\
40 u[1:-1:2,2:-2:2]**2-f[1:-1:2,2:-2:2]
41 u[1:-1:2,2:-2:2]-=res[1:-1:2,2:-2:2]/(
42 -4.0/h2+2*u[1:-1:2,2:-2:2])
43
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
228 Case Study: Multigrid
44 return u
45
46
47 def solve(rhs):
48 h=0.5
49 u=np.zeros_like(rhs)
50 fac=2.0/h**2
51 dis=np.sqrt(fac**2+rhs[1,1])
52 u[1,1]=-rhs[1,1]/(fac+dis)
53 return u
The first function get_lhs takes a current u and step-length squared h2 and returns
L(u) for this level. The second function gs_rb_step requires in addition the right-hand
side f and combines a Newton–Raphson iteration with a red–black Gauss–Seidel one
to produce a smoothing step. Note that nowhere do we need to specify the sizes of the
arrays: the information about which level is being used is contained in the h2 parameter.
The final function returns the exact solution of the discretized equation on the coarsest
3 × 3 grid.
These are the most lengthy and complicated functions we shall need. By abstracting
them into a separate module, we can construct a test suite (not shown) in order to ensure
that they deliver what is intended.
3 import numpy as np
4 from util import l2_norm, restrict_hw, prolong_lin
5 from smooth import gs_rb_step, get_lhs, solve
6
7 class Grid:
8 """
9 A Grid contains the structures and algorithms for a
10 given level together with a pointer to a coarser grid.
11 """
12 def __init__(self,name,steplength,u,f,coarser=None):
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
10.4 A Simple Python Multigrid Implementation 229
13 self.name=name
14 self.co=coarser # pointer to coarser grid
15 self.h=steplength # step-length h
16 self.h2=steplength**2 # h**2
17 self.u=u # improved variable array
18 self.f=f # right-hand-side array
19
20 def __str__(self):
21 """ Generate an information string about this level. """
22 sme=’Grid at %s with steplength = %0.4g\n’ % (
23 self.name, self.h)
24 if self.co:
25 sco=’Coarser grid with name %s\n’ % self.co.name
26 else:
27 sco=’No coarser grid\n’
28 return sme+sco
29
30 def smooth(self,nu):
31 """
32 Carry out Newton--Raphson/Gauss--Seidel red--black
33 iteration u-->u, nu times.
34 """
35 print ’Relax in %s for %d times’ % (self.name,nu)
36 v=self.u.copy()
37 for i in range(nu):
38 v=gs_rb_step(v,self.f,self.h2)
39 self.u=v
40
41 def fas_v_cycle(self,nu1,nu2):
42 """ Recursive implementation of (nu1, nu2) FAS V-Cycle."""
43 print ’FAS-V-cycle called for grid at %s\n’ % self.name
44 # Initial smoothing
45 self.smooth(nu1)
46 if self.co:
47 # There is a coarser grid
48 self.co.u=restrict_hw(self.u)
49 # Get residual
50 res=self.f-get_lhs(self.u,self.h2)
51 # Get coarser f
52 self.co.f=restrict_hw(res)+get_lhs(self.co.u,
53 self.co.h2)
54 oldc=self.co.u
55 # Get new coarse solution
56 newc=self.co.fas_v_cycle(nu1,nu2)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
230 Case Study: Multigrid
57 # Correct current u
58 self.u+=prolong_lin(newc-oldc)
59 self.smooth(nu2)
60 return self.u
61
62 def fmg_fas_v_cycle(self,nu0,nu1,nu2):
63 """ Recursive implementation of FMG-FAS-V-Cycle"""
64 print ’FMG-FAS-V-cycle called for grid at %s\n’ % self.name
65 if not self.co:
66 # Coarsest grid
67 self.u=solve(self.f)
68 else:
69 # Restrict f
70 self.co.f=restrict_hw(self.f)
71 # Use recursion to get coarser u
72 self.co.u=self.co.fmg_fas_v_cycle(nu0,nu1,nu2)
73 # Prolong to current u
74 self.u=prolong_lin(self.co.u)
75 for it in range(nu0):
76 self.u=self.fas_v_cycle(nu1, nu2)
77 return self.u
Thanks to the use of classes, the code is remarkably simple and concise. Note that
None in line 12 is a special Python variable which can take any type and always eval-
uates to zero or False. By default there is not a coarser grid, and so when we actually
construct a list of levels we need to link manually each grid to the next coarser one.
The function __init__, lines 12–18, constructs a Grid level. The function __str__,
lines 20–28, constructs a string describing the current grid. Their use will be described
soon. Lines 30–39 give the smooth function, which simply carries out nu iterations of
the Newton–Raphson/Gauss–Seidel smoothing process on the current grid. The func-
tion fas_v_cycle on lines 41–60 is non-trivial. If we are at the coarsest grid level, it
ignores lines 47–58 and simply carries out ν1 + ν2 smoothing steps and then returns. In
the more general case, lines 45–54 carry out ν1 “smooth” steps followed by a “coarsen”
step of the two-grid scheme. Line 56 then carries out the “solve” step by recursion, and
finally lines 58 and 59 carry out the remaining “correct” and finally ν2 “smooth” steps.
It is not the most efficient code. For useful hints on achieving efficiency, see the dis-
cussion of an equivalent code in Press et al. (2007). Finally, lines 62–77 implement a
non-trivial function for carrying out full multigrid FAS cycles. The parameter ν0 con-
trols how many V-cycles are carried out. Notice how closely the Python code mimics
the earlier theoretical description, which makes it easier to understand and to develop, a
great saving of time and effort.
We shall assume that the code snippet above has been saved as grid.py.
Finally, we create the following snippet in a file rungrid.py to show how to use
the functions. We have chosen to replicate the worked example from Press et al. (2007)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
10.4 A Simple Python Multigrid Implementation 231
Figure 10.5 The left-hand side of equation (10.15) plotted on the 32 × 32 computational grid.
The original source was zero everywhere apart from the central point, where it took the value 2.
which studies (10.29) on a 32 × 32 grid with f (x, y) = 0 except at the mid-point, where
it takes the value 2.
3 import numpy as np
4 from grid import Grid
5 from smooth import get_lhs
6
7 n_grids=5
8 size_init=2**n_grids+1
9 size=size_init-1
10 h=1.0/size
11 foo=[] # container for grids
12
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
232 Case Study: Multigrid
36 foo[0].fmg_fas_v_cycle(1,1,1)
37
40 lhs=get_lhs(foo[0].u,foo[0].h2)
41
46 plt.ion()
47 fig=plt.figure()
48 ax=Axes3D(fig)
49
50 xx,yy=np.mgrid[0:1:1j*size_init,0:1:1j*size_init]
51 ax.plot_surface(xx,yy,lhs,rstride=1,cstride=1,alpha=0.4)
Lines 7–11 set up initial parameters, including an empty list foo, which is a container
for the Grid hierarchy. Each pass of the loop in lines 13–20 constructs a Grid (line
17) and adds it to the list (line 18). Next the loop in lines 22 and 23 links Grids to the
coarser neighbour. Lines 26–27 check that nothing is amiss by exercising the __str__
function. The lines 30–34 set up some initial data for the problem actually treated by
Press et al. (2007). Finally, line 36 constructs the solution.
In order to perform one (of many) checks on the solution, we reconstruct the left-hand
side of the equation evaluated on the finest grid in line 40. The remainder of the snippet
plots this as a surface, shown in Figure 10.5. The initial source was zero everywhere
except at the central point where it took the value 2. The numerical results, which are
identical to those obtained with the code of Press et al. (2007), give the peak value as
1.8660. Increasing the grid resolution to 64 × 64 or 128 × 128 does not change this value
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
10.4 A Simple Python Multigrid Implementation 233
significantly. The L2 -norm of the error is always O(h2 ), but this disguises the effect of
the discontinuity.
This example was chosen to run in a fraction of a second on most reasonably modern
machines. If you hack the code to handle a more realistic (and complicated) problem
for you, it might run rather more slowly. How do we “improve” the code? The first
step is to verify carefully that the code does produce convergent results. Next we might
think about improving the efficiency of the Python code. The first step is to profile the
code. Assuming you are using IPython, you should replace the command run rungrid
with run -p rungrid. This appends to the output the results from the Python profiler
which show the time spent in each function. If there is a “bottleneck”, it will show up at
the top of the list. Almost certainly, any bottleneck will be in the file smooth.py. It may
be that the Python code for such functions is obviously inefficient and can be improved.
More rarely, vectorized NumPy code is too slow. Then the simplest effective solution is
to use f2py, as advocated in Chapter 9. This does not mean recoding the whole program
in Fortran, only the bottleneck functions, which should be computationally intensive but
with a simple overall structure, so that no great Fortran expertise is needed.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.012
Appendix A Installing a Python
Environment
In order to use Python we need to carry out two tasks. The first is the obvious one of
downloading and installing the relevant software. This is an easy, straightforward task,
provided one avoids the less-than-obvious pitfalls. Section A.1 shows how to achieve
this.
The second task is to determine how to communicate with the Python environment
both effectively and efficiently. This is much harder for your author to prescribe because
it depends also on the expertise and the ambitions of the reader. Users of Mathematica
or Matlab do not face this dilemma, for they are offered just the one interface to their
proprietary software. Because it is open-source software, myriads of developers have
produced an apparent plethora of ways to interact with Python. The wise reader should
expect a trade-off between simplicity and rigidity for the beginner, akin to the com-
mercial examples cited above, and complexity with versatility, more suited to the more
experienced and ambitious user.
The next two sections review and suggest how to use them to access your Python
environment. My advice is to start where you feel most comfortable, and then come
back and advance if and when your ambitions demand progress.
If you have not used the Jupyter notebook before, then you should certainly review
Section A.2.2. This describes the many and varied facilities this software offers, without
getting into the details of IPython, which are discussed in detail in Chapter 2. Finally,
Section A.5 looks at how to enhance your Python environment to suit your own partic-
ular needs.
As has been outlined in Section 1.2, we shall need not only core Python, but also the add-
on packages IPython (see Chapter 2), NumPy, SciPy (packages discussed in Chapter 4),
Matplotlib (see Chapter 5), potentially Mayavi (discussed in Chapter 6) and SymPy
(reviewed in Chapter 7). Although data analysis is merely mentioned in Section 4.5,
that section recommends the Pandas package, and for many this will be a must-have.
The core Python package may already be available on your machine, and it is very
tempting to load up the others from your favourite software repository, or from the main
Python source,1 where you can find both documentation and downloadable packages
1 Its website is http://www.python.org.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.013
236 Installing a Python Environment
for the major platforms. In my experience, this is far too often the advice offered by
“computer officers”, who are oblivious of the consequences of this policy. The snag with
this approach is that it is very difficult to ensure that all of these packages are mutually
compatible. Even if they are at one instant, a subsequent “upgrade improvement” to,
say, NumPy may well break the link to the other add-on packages. Instead, I recommend
strongly that you download a complete integrated system, and stick with it. The best of
these allow incremental updates, but these updates do take into account and resolve, the
compatibility issues. Alternatively, you may wish to upgrade the complete system as a
whole every other year or so.
There are a number of integrated systems available, so which one should you choose?
Fortunately there are two outstanding systems which are very popular.
The earlier of these is the Enthought Python Distribution.2 New users should note
that it is now called Canopy, and is available on all of the standard platforms. Every
user can download and use free-of-charge Canopy Express. This is a “light” version of
Canopy, omitting many specialized packages. It does, however, contain all of the pack-
ages mentioned above except Mayavi. However, if you are a student or staff member at
a degree-granting institution, you can obtain a free-of-charge academic licence, renew-
able annually, for the full Canopy package, which of course includes Mayavi. If you
are ineligible for the academic licence, then you need to do a careful cost–benefit anal-
ysis because Canopy offers a wide range of goodies, intended for a business-oriented
audience, which might justify the cost of a licence.
More recently the Anaconda Python Distribution3 has appeared, and has become
very popular, especially with Windows users. It gives free access to all of the packages
mentioned above, although some users report difficulties with Mayavi.
If you choose one of these systems you gain a huge advantage: they are well-tested
and popular integrated environments out-of-the-box. All of the code in this book has
been generated and tested seamlessly using both of them.
There exist similar free-to-use packages, including Python(x,y)4 for the Windows
platform only, which includes all of the recommended packages, including Mayavi,
Scipy-Superpack5 for Mac OS X and Source Python Distribution6 for many platforms,
and they may well suit your requirements better, but I have not tested any of these
packages.
As a scientist, the reader might well ask which is the right distribution for me? Per-
haps I should download both Anaconda and Canopy in order to make an informed
choice? Unless you are an experienced user, the short answer is “don’t!”. Each distribu-
tion works to ensure that its Python components are the default ones. More experienced
users who want to run both distributions will need to locate, e.g., the ipython executa-
bles for both. Then they will need to set up suitable aliases in, say, the .bashrc file, in
order to distinguish them.
2 It is available at http://www.enthought.com/products/epd.
3 It has a home page at https://www.continuum.io.
4 Its website is http://code.google.com/p/pythonxy.
5 This moves around but is currently at http://fonnesbeck.github.io/ScipySuperpack.
6 It can be found at http://code.google.com/p/spdproject/downloads/list.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.013
A.2 Communication with IPython Using the Jupyter Notebook 237
In order to use parts of SciPy and the f2py utility, a Fortran compiler is needed. If
you already have one, then the Python integrated system should recognize it. Otherwise,
the ideal companion is gfortran, and free-to-use binaries for all platforms are available
from its website.7 Although new users can manage without it, it is highly recommended
to become familiar with the use of the command line interface (CLI) on your com-
puter. In Linux the CLI is available as the xterm application, while in Mac OS X it
has migrated to the Terminal.app, which is available in the “Utilities” subdirectory of
the “Applications” folder. In Windows 10 it is the “Command Prompt Window”. These
accept human input from the keyboard, very often in highly abbreviated form (in or-
der to minimize the number of keystrokes), which, while highly efficient, requires both
knowledge and experience of the user.
This is the clear recommendation for users new to Python. It offers an interface8 similar
to the Mathematica notebook, but with one major difference. Here one needs to open
two applications, both IPython itself and the notebook which utilizes your system’s
default browser.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.013
238 Installing a Python Environment
and execute the code by the key combination shift + ret. The answer appears in an
“output cell”. Next click back into the input cell and amend the command to
(2+3)*(13-5)
and re-execute the code. The value in the output cell changes from 45 to 40. The begin-
ner might like to try (a) repeating this exercise but using the key combination ctl+ret,
which minimizes the creation of new input cells, and (b) a few other calculator exam-
ples.
One could try more complicated interactions using the menus and/or icons in the
menu bar. However, for speedy serious use, I recommend mastering the keyboard short-
cuts discussed in the next paragraph.
Notebook commands can be divided into two types, edit mode, which modifies a
single cell, and command mode, which manipulates the entire notebook.
In edit mode the contents of a cell can be modified using pretty standard editing
commands. However, command mode requires some more unusual actions. The most
important of these are summarized in Table A.1.
We are now able to enter code into notebook cells. Precisely what code you should
enter is of course the subject of this book and the reader is referred on to Chapter 2.
However, we can also use cells both to enter explanatory text and to give structure to
the notebook. The notebook allows the use of a dialect of the Markdown language11
to insert rich text into cells. The next two subsections summarize the most relevant
features.
11 See the project page at http://daringfireball.net/projects/markdown/syntax for an
introduction to the syntax. The site http://markdowntutorial.com contains a short whimsical
hands-on introduction.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.013
A.2 Communication with IPython Using the Jupyter Notebook 239
Keypress Action
h List the keyboard shortcuts
s Save the notebook
a Insert a new cell above the current one
b Insert a new cell below the current one
x Cut a cell
c Copy a cell
v Paste cell below the current one
shift + v Paste cell above the current one
d , twice Delete current cell
z Undo a delete action
l Toggle line numbering
y Change current cell to IPython code mode
m Change current cell to Markdown mode
1,2,. . . ,6 Change current cell to appropriate heading size
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.013
240 Installing a Python Environment
Now press shift + ret to convert the cell to formatted text. Note that you do not need a
full LATEX installation since IPython includes its own mini version. Of course Markdown
contains more elaborate constructs, e.g., lists, tables and references, and the reader is
directed to the references in the footnote.
Here FORMAT should be replaced by html or pdf. (The latter choice will work only if
there is a full LATEXpackage in a standard location.) Various parameters can be set: see
the documentation12 for details.
IPython can be invoked from the command line by the simple instruction ipython
plus ret. This produces a header and a line labelled In [1]:. Here is the point where
IPython is ready to accept commands. The final command should be exit followed by
return, which quits the interpreter.
Built into IPython is the GNU readline utility. This means that, on the interpreter’s
current line, the left- and right-arrow keys move the cursor appropriately, and deletion
and insertion are straightforward. Just about the only additional commands I ever use
are ctl-a and ctl-e, which move the cursor to the start and end of the line, and ctl-
k, which deletes the material to the right of the cursor. The up- and down-arrow keys
replicate the previous and next command lines respectively, so that they can be edited
or reused.13 You should try out some of these features. Try (2+3)*(4+5) followed by
ret, to see the corresponding output, followed by a new input line. Now try amending
the command. First use the up-arrow key to recover the previous command, edit it to
read (2+3)*(13-5), and execute it. However, there is no way to edit two different lines
simultaneously in terminal mode. For this one needs a text editor.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.013
A.3 Communication with IPython Using Terminal Mode 241
keyboard has to be converted to a number, and the current standard is “Unicode”, which
carries conversion codes for more than 110,000 characters. Of course, the conversion
works also in the reverse direction for output to the screen or printer, so the casual user
never sees this complexity.
Programming languages predate word processors and were developed when com-
puter memory was scarce. The standard conversion is “ASCII”, which allows for 93
printable characters, more than enough for most programming languages, including
Python. Unfortunately, Unicode-based editors make a very poor job of producing ASCII
and in practice we need to use an ASCII-based editor.
Before choosing a “programmer’s editor”, you should take stock of which program-
ming languages you use or intend to use. The reason for this is that all of these languages
have their idiosyncrasies, and it is highly desirable that the editor be aware of them.14
Fortunately, there are excellent ASCII-based editors in the public domain. Emacs
and vim (formerly vi) are ultra-versatile and are available on almost all platforms. Their
versatility depends on built-in or add-on modules for the main programming languages,
written by enthusiasts. However, the cost of that versatility is a slightly steeper learning
curve. Perhaps this is the reason why many users of one or the other are fiercely (almost
irrationally) loyal to their choice and deprecate strongly the other.
Of course, many scientists will be prepared to sacrifice some aspects of versatility in
return for a much shallower learning curve, and there are some extremely competent
open-source alternatives. In this context, many Python users on the Windows platform
prefer to use Notepad++,15 which is a versatile editor with a much shallower learning
curve, but it is native to the Windows platform. The editor TextWrangler,16 which is in-
trinsic to the Mac OS X platform, is similarly the preferred choice. There are of course
many other excellent public-domain and proprietary programmer’s editors which are
aware of the common programming languages. Tables listing and comparing their fea-
tures can be found on the internet.
As can be seen in Chapter 2, non-trivial program development involves frequent
switching between the IPython interpreter and the text editor. There are three different
ways of doing this. Which you choose depends on a trade-off between user convenience
and the complexity of the initial setup process.
14 For example, in the creation of this book I used LATEX, Python, Reduce, Fortran and C++. Fairly
obviously, I prefer a single editor which “understands” all of these.
15 Its website is http://notepad-plus-plus.org.
16 Free-to-use from http://www.barebones.com/products/textwrangler.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.013
242 Installing a Python Environment
The Mathematica and Matlab examples mentioned at the start of this appendix are very
simple examples of an integrated development environment (IDE), where an ASCII-
based editor is chosen in advance. In these two cases, the editor knows only about
the one programming language. However, most IDEs were constructed to understand
several languages, and to offer extra features to aid the programmer. For serious devel-
opment work in the Fortran and C families of languages they are extremely valuable.
There are of course very many IDEs available, some as commercial software and
some as open-source software. A very polished commercial example is Pycharm,18
which, in fact, is free for academic users. Almost as good is the Spyder IDE, which
is included in the Anaconda and Canopy distributions recommended above. Readers
who use several other programming languages may wish to consider an IDE, such as
the two mentioned above. However, very many users will find the Jupyter notebook,
which is included in every distribution, described in Section A.1 to be the ideal choice.
17 Unfortunately, the official documentation is far from adequate. I got started by searching the web for
emacs ipython. An easily overlooked point is that to use python mode you need to tell the editor
where to find the Python executable. You should give instead the address of the IPython executable.
18 Information can be found at https://www.jetbrains.com.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.013
A.5 Installing Additional Packages 243
As you develop confidence in using Python for your chosen research field, you may need
to install additional Python modules. The first approach is to check whether a particular
module is included, but not installed, in your chosen distribution. If so, follow your
distribution’s instructions to implement it.
If you still need to find it, the next strategy is to investigate the central repository, the
Python Package Index,19 which contains more than 25,000 add-on packages. Bear in
mind that there are many excellent packages, some recommended in this book, which
are not in that repository!
There are various ways of installing them, each “easier than the previous method”,
but the following procedure is simple and should work with any Python distribution.
Suppose you have located a package, say foo.tar.gz, and have downloaded it to
a convenient directory. After unpacking it, e.g., using gzip, you should move to the
source directory foo, which will contain several files, including one called setup.py.
Assuming you have, or have obtained, “super user” privileges, the command line
python setup.py install
will compile and install the package where your Python interpreter can find it, typically
with a command such as
import foo
19 Currently it is at http://pypi.python.org/pypi.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.013
Appendix B Fortran77 Subroutines for
Pseudospectral Methods
The next subroutine is the fast discrete Fourier transform given on pages 176–179,
together with a detailed explanation of how it works. Lines 21–23 contain additional
comments to aid the f2py tool.
1 SUBROUTINE FFT (A,B,IS,N,ID)
2 C-- +--------------------------------------------------------------
3 C-- | A CALL TO FFT REPLACES THE COMPLEX DATA VALUES A(J)+i B(J),
4 C-- | J=0,1,... ,N-1 WITH THEIR TRANSFORM
5 C-- |
6 C-- | 2 i ID PI K J / N
7 C-- | SUM (A(K) + iB(K))e , J=0,1,.. .,N-1
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.014
F77 Pseudospectral Code 245
8 C-- | K=0..N-1
9 C-- |
10 C-- | INPUT AND OUTPUT PARAMETERS
11 C-- | A ARRAY A (0: *), REAL PART OF DATA/TRANSFORM
12 C-- | B ARRAY B (0: *), IMAGINARY PART OF DATA/TRANSFORM
13 C-- | INPUT PARAMETERS
14 C-- | IS SPACING BETWEEN CONSECUTIVE ELEMENTS IN A AND B
15 C-- | (USE IS=+1 FOR ELEMENTS STORED CONSECUTIVELY)
16 C-- | N NUMBER OF DATA VALUES, MUST BE A POWER OF TWO
17 C-- | ID USE +1 OR -1 TO SPECIFY DIRECTION OF TRANSFORM
18 C-- +--------------------------------------------------------------
19 IMPLICIT REAL*8 (A-H,O-Z)
20 DIMENSION A(0:*),B(0:*)
21 Cf2py intent(in, out) A, B
22 Cf2py intent(in) IS, ID
23 Cf2py integer intent(hide), depend(A) N
24 J=0
25 C--- APPLY PERMUTATION MATRIX ----
26 DO 20 I=0,(N-2)*IS,IS
27 IF (I.LT.J) THEN
28 TR = A(J)
29 A(J) = A(I)
30 A(I) = TR
31 TI = B(J)
32 B(J) = B(I)
33 B(I) = TI
34 ENDIF
35 K = IS*N/2
36 10 IF (K.LE.J) THEN
37 J = J-K
38 K = K/2
39 GOTO 10
40 ENDIF
41 20 J = J+K
42 C--- PERFORM THE LOG2 N MATRIX-VECTOR MULTIPLICATIONS ---
43 S = 0.0D0
44 C = -1.0D0
45 L = IS
46 30 LH=L
47 L = L+L
48 UR = 1.0D0
49 UI = 0.0D0
50 DO 50 J=0,LH-IS,IS
51 DO 40 I=J,(N-1)*IS,L
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.014
246 F77 Pseudospectral Code
52 IP = I+LH
53 TR = A(IP)*UR-B(IP)*UI
54 TI = A(IP)*UI+B(IP)*UR
55 A(IP) = A(I)-TR
56 B(IP) = B(I)-TI
57 A(I) = A(I)+TR
58 40 B(I) = B(I)+TI
59 TI = UR*S+UI*C
60 UR = UR*C-UI*S
61 50 UI=TI
62 S = SQRT (0.5D0*(1.0D0-C))*ID
63 C = SQRT (0.5D0*(1.0D0+C))
64 IF (L.LT.N*IS) GOTO 30
65 RETURN
66 END
Next we include the fast discrete Fourier cosine transform from pages 182–183. We
have added comment lines 22–24 as above.
1 SUBROUTINE FCT (A,X,N,B)
2 C-- +--------------------------------------------------------------
3 C-- | A CALL TO FCT PLACES IN B(0:N) THE COSINE TRANSFORM OF THE
4 C-- | VALUES IN A(0:N)
5 C-- |
6 C-- | B(J) = SUM C(K)*A(K)*COS(PI*K*J/N) , J=0,1,...,N, K=0..N
7 C-- |
8 C-- | WHERE C(K) = 1.0 FOR K=0,N, C(K) =2.0 FOR K=1,2,...,N-1
9 C__ |
10 C-- | INPUT PARAMETERS:
11 C-- | A A(0:N) ARRAY WITH INPUT DATA
12 C-- | X X(0:N) ARRAY WITH CHEBYSHEV GRID POINT LOCATIONS
13 C-- | X(J) = -COS(PI*J/N) , J=0,1,...,N
14 C-- | N SIZE OF TRANSFORM - MUST BE A POWER OF TWO
15 C-- | OUTPUT PARAMETER
16 C-- | B B(0:N) ARRAY RECEIVING TRANSFORM COEFFICIENTS
17 C-- | (MAY BE IDENTICAL WITH THE ARRAY A)
18 C-- |
19 C-- +--------------------------------------------------------------
20 IMPLICIT REAL*8 (A-H,O-Z)
21 DIMENSION A(0:*),X(0:*),B(0:*)
22 Cf2py intent(in) A, X
23 Cf2py intent(out) B
24 Cf2py integer intent(hide), depend(A) N
25 N2 = N/2
26 A0 = A(N2-1)+A(N2+1)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.014
F77 Pseudospectral Code 247
27 A9 = A(1)
28 DO 10 I=2,N2-2,2
29 A0 = A0+A9+A(N+1-I)
30 A1 = A( I+1)-A9
31 A2 = A(N+1-I)-A(N-1-I)
32 A3 = A(I)+A(N-I)
33 A4 = A(I)-A(N-I)
34 A5 = A1-A2
35 A6 = A1+A2
36 A1 = X(N2-I)
37 A2 = X(I)
38 A7 = A1*A4+A2*A6
39 A8 = A1*A6-A2*A4
40 A9 = A(I+1)
41 B(I ) = A3+A7
42 B(N-I) = A3-A7
43 B(I+1 ) = A8+A5
44 10 B(N+1-I) = A8-A5
45 B(1) = A(0)-A(N)
46 B(0) = A(0)+A(N)
47 B(N2 ) = 2.D0*A(N2)
48 B(N2+1) = 2.D0*(A9-A(N2+1))
49 CALL FFT(B(0),B(1),2,N2,1)
50 A0 = 2.D0*A0
51 B(N) = B(0)-A0
52 B(0) = B(0)+A0
53 DO 20 I=1,N2-1
54 A1 = 0.5 D0 *(B(I)+B(N-I))
55 A2 = 0.25D0/X(N2+I)*(B(I)-B(N-I))
56 B(I ) = A1+A2
57 20 B(N-I) = A1-A2
58 RETURN
59 END
Finally, we add three subroutines from pages 188–189 which convert arrays in phys-
ical space to those in Chebyshev space and vice versa, and carry out the spatial differ-
entiation in Chebyshev space.
1 SUBROUTINE FROMCHEB (A,X,N,B)
2 IMPLICIT REAL*8 (A-H,O-Z)
3 DIMENSION A(0:N),X(0:N),B(0:N)
4 Cf2py intent(in) A, X
5 Cf2py intent(out) B
6 Cf2py integer intent(hide), depend(A) N
7 B(0) = A(0)
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.014
248 F77 Pseudospectral Code
8 A1 = 0.5D0
9 DO 10 I=1,N-1
10 A1 = -A1
11 10 B(I) = A1*A(I)
12 B(N) = A(N)
13 CALL FCT(B,X,N,B)
14 RETURN
15 END
16
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.014
F77 Pseudospectral Code 249
52 RETURN
53 END
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:38, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.014
References
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.015
References 251
Langtangen, H. P. (2014), A Primer on Scientific Programming with Python, fourth edn, Springer.
Lutz, M. (2013), Learning Python, fifth edn, O’Reilly.
Mackey, M. C. and Glass, L. (1977), ‘Oscillation and chaos in physiological control systems’,
Science 197, 287–289.
Matplotlib Community (2016), ‘Matplotlib release 1.5.1’, available from
http://matplotlib.org/Matplotlib.pdf.
McKinney, W. W. (2012), Python for Data Analysis, O’Reilly.
Murray, J. D. (2002), Mathematical Biology I. An Introduction, Springer.
NumPy Community (2017a), ‘NumPy user guide release 1.12’, available from https://docs.
scipy.org/doc/numpy/.
NumPy Community (2017b), ‘NumPy reference release 1.12’, available from https://docs.
scipy.org/doc/numpy/.
Øksendal, B. (2003), Stochastic Differential Equations, sixth edn, Springer.
Peitgen, H.–O. and Richter, P. H. (1986), The Beauty of Fractals: Images of Complex Dynamical
Systems, Springer.
Press, W. H., Teukolsky, S. A., Vetterling, W. T. and Flannery, B. P. (2007), Numerical Recipes:
The Art of Scientific Computing, third edn, Cambridge.
Ramachandandran, P. and Variquaux, G. (2009), ‘Mayavi user guide release 3.3.1’,
available from http://code.enthought.com/projects/mayavi/docs/development/
latex/mayavi/mayavi_user_guide.pdf.
Rossant, C. (2015), Learning IPython for Interactive Computing and Data Visualization, second
edn, Packt Publishing.
SciPy Community (2017), ‘SciPy reference guide release 0.19’, available from https://docs.
scipy.org/doc/.
Sparrow, C. (1982), The Lorenz Equations, Springer.
Tosi, S. (2009), Matplotlib for Python Developers, Packt Publishing.
Trefethen, L. N. (2000), Spectral Methods in MATLAB, SIAM.
Trottenberg, U., Oosterlee, C. W. and Schüller, A. (2001), Multigrid, Academic Press.
van Rossum, G. and Drake Jr., F. L. (2011), An Introduction to Python, Network Theory Ltd.
Wesseling, P. (1992), An Introduction to Multigrid Methods, Wiley.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241.015
Hints for Using the Index
The main purpose of this index is to identify precisely where Python language concepts
are introduced.
There is, however, a major problem. Take, for example, the plot() function. Four
different functions of this name are introduced in this book. In order to locate the re-
quired definition, the user will need to specify whether it is the Matplotlib.pyplot one,
the Matplotlib Axes one, the Mayavi mlab one or the SymPy one. Even the humble
exponential function is used both in its NumPy and SymPy forms.
In the main text, ambiguity was avoided by prefixing the function name by the class
name, plus a dot, for this is how Python distinguishes them. The more or less standard
prefixes are shown in the table, and this is how the functions can be found in the index.
Class Prefix
NumPy np.
Matplotlib.pyplot plt.
Matplotlib.Axes ax.
SymPy sy.
SymPy plotting syp.
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241
Index
∗args, 43 class
∗∗kwargs, 44 add , 49
adaptive time-stepping, 152 attribute, 49
aliasing, 219 Frac, 48
Anaconda Python Distribution, 236 Grid, 228
append(), 29 init , 49
argument, 40 instance, 49
ax.annotate(), 95 mul , 49
ax.clabel(), 99 str , 49
ax.contour(), 99 close(), 70
ax.contourf(), 99 cmath, 26, 27
ax.errorbar(), 94 code snippets
ax.grid(), 91 website, 5
ax.imshow(), 100 colnew(), 165
plt.legend(), 91 comment, 22
ax.loglog(), 90 complex
ax.plot(), 88 complex(), 26
ax.plot(), 88 conjugate(), 27
ax.scatter, 117 imag(), 27
ax.semilogx(), 90 real(), 27
ax.semilogy(), 90 complex, 26
ax.set title(), 91 component-wise, 59
ax.set xlabel(), 91 continue, 37
ax.set xlim(), 91 convergence
ax.set xticks(), 91 exponential, 196
ax.set ylabel(), 91 copy, 32
ax.set yticks(), 91 deep, 32
ax.text(), 95 shallow, 32
axis, 65 dde23(), 173
Black–Scholes equation, 186 def, 40
block, 21 dictionary, 33
bool, 26 diff(), 195
Bratu problem, 167 division
break, 37 integer, 24
broadcasting, 65 real, 25
first rule, 65 docstring, 18, 22, 40
second rule, 66 dsolve(), 142
Burgers’ equation, 192, 208 dtype, 65
kink solution, 208 elif, 35
Canopy, 236 else, 35, 37
Chebyshev Emacs, 241
grid nodes, 200 Enneper’s minimal surface, 117, 124, 125
polynomials, 200 Enthought Python Distribution, 236
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241
254 Index
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241
Index 255
math, 25 np.linalg.det(), 78
Matplotlib, 82 np.linalg.eig(), 78
back-end, 84 np.linalg.inv(), 78
classes np.linalg.solve(), 79
Axes, 87 np.linspace(), 57
Figure, 87 np.load(), 72
front-end, 83 np.loadtxt(), 72
pylab, 83 np.logspace(), 57
pylplot, 83 np.lookfor, 56
mplot3d, 117 np.max(), 74
preferences file, 84 np.mean(), 75
rc, 97 np.median(), 75
styles np.meshgrid(), 67
colour, 89 np.mgrid(), 67
line, 89 np.min(), 74
marker, 90 np.nan, 61
Widgets, 112 np.nanmax(), 74
matrix np.nanmin(), 74
arithmetic, 78 np.NINF, 61
eigenvalues, 78 np.ogrid(), 68
eigenvectors, 78 np.ones(), 58
Maxima, 130 np.ones like(), 58
Mayavi, 111 np.piecewise(), 64
mlab, 117 np.poly1d(), 199
method of lines, 193 np.polyfit(), 199
Milstein’s method, 186 np.polyval(), 76
modes np.prod(), 75
rough, 215 np.ptp(), 74
smooth, 215 np.reshape(), 68
module, 27 np.roots(), 76
mutable, 31 np.save(), 72
np.savetxt(), 72
namespace, 27, 40
np.savez(), 72
narrow, 25
np.select(), 63
nbconvert, 240
np.std(), 75
ndim, 65
np.sum(), 75
Newton–Raphson iteration, 221
np.var(), 75
None, 40
np.vstack(), 78
Notepad++, 241
np.zeros(), 58
np, 56
np.zeros like(), 58
np.arange(), 57
NumPy, 55
np.array(), 58
ndarray, 55
np.array, 65
image form, 67 object, 22
matrix form, 66 identifier, 22
np.average(), 75 identity, 22
np.cumprod(), 75 type, 23
np.cumsum(), 75 type(), 23
np.dot(), 66 object oriented, 50
np.empty(), 58 odeint, 153
np.empty like(), 58 open(), 70
np.eye(), 77 pandas, 73
np.hstack(), 59 pass, 35, 40
np.identity(), 77 penalty method, 209
np.inf, 61 phase plane portrait, 156
np.isfinite(), 74 plt.figure(), 87
np.isnan(), 74 plt.get backend(), 84
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241
256 Index
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241
Index 257
ufunc, 60
V-cycle, 222
van der Pol equation, 150, 158
Vim, 241
Visualization Tool Kit (VTK), 110
while loop, 39
widen, 25
Wiener process, 179
xrange(), 53
zip(), 70
Downloaded from https://www.cambridge.org/core. ETH-Bibliothek, on 07 Jan 2022 at 09:13:37, subject to the Cambridge Core terms of use, available at
https://www.cambridge.org/core/terms. https://doi.org/10.1017/9781108120241