The R package viscomplexr has been written as a visualization tool for complex functions. More precisely, it provides functionality for making phase portraits of such functions. The method, sometimes called domain coloring, exists in many sub-varieties. However, from the author’s point of view, the style proposed by E. Wegert in his book Visual Complex Functions (Wegert 2012) comes with a particular clarity and a special aesthetic appeal at the same time. Therefore, this package closely follows Wegert’s conventions. Conceptually, the package is intended for being used inside the framework of R’s base graphics, i.e. users of this package can freely utilize all features of base graphics for obtaining an optimum result, be it for scientific or artistic purposes. This vignette is not at all an introduction to function theory or an exhaustive treatment of what can be done with phase portraits - I recommend Wegert’s book for an ideal combination of both; the purpose of this vignette is in fact to make the reader acquainted with the technical features the package provides in a step-by-step process.
Due to the size restriction of CRAN packages, the number of illustrations in this vignette is kept to a minimum. Readers are encouraged to run all code examples shown below (and hopefully enjoy what they see), but especially those where we explicitly invite them to do so. Alternatively, visit the package’s website for a richly illustrated version of this vignette.
The package does not contain many functions, but provides a very versatile workhorse called phasePortrait. We will explore some of its key features now. Let us first consider a function that maps a complex number \(z \in \mathbb{C}\) on itself, i.e. \(f(z)=z\). After attaching the package with library(viscomplexr)
, a phase portrait of this function is obtained very easily with:
phasePortrait("z", xlim = c(-8.5, 8.5), ylim = c(-8.5, 8.5),
xlab = "real", ylab = "imaginary", main = "f(z) = z",
nCores = 2) # Probably not required on your machine (see below)
# Note the argument 'nCores' which determines the number of parallel processes to
# be used. Setting nCores = 2 has been done here and in all subsequent
# examples as CRAN checks do not allow more parallel processes.
# For normal work, we recommend not to define nCores at all which will make
# phasePortrait use all available cores on your machine.
# The progress messages phasePortrait is writing to the console can be
# suppressed by including 'verbose = FALSE' in the call (see documentation).
Such a phase portrait is based on the polar representation of complex numbers. Any complex number \(z\) can be written as \(z=r\cdot\mathrm{e}^{\mathrm{i}\varphi}\) or equivalently \(z=r\cdot(\cos\varphi+\mathrm{i}\cdot\sin\varphi)\), whereby \(r\) is the modulus and the angle \(\varphi\) is the argument. The argument, also called the phase angle, is the angle in the origin of the complex number plane between the real axis and the position vector of the number in counter-clockwise orientation. The main feature of a phase portrait is to translate the argument into a color. In addition, there are options for visualizing the modulus or, more precisely, its relative change.
The translation of the phase angle \(\varphi\) into a color follows the hsv color model, where radian values of \(\varphi=0+k\cdot2\pi\), \(\varphi=\frac{2\pi}{3}+k\cdot2\pi\), and \(\varphi=\frac{4\pi}{3}+k\cdot2\pi\) with \(k\in\mathbb{Z}\) translate into the colors red, green, and blue, respectively, with a continuous transition of colors with values between. As all numbers with the same argument \(\varphi\) obtain the same color, the numbers of the complex plane as visualized in the Figure above are colored along the chromatic cycle. In order to add visual structure, argument values of \(\varphi=\frac{2\pi}{9}\), i.e. \(40°\) and their integer multiples are emphasized by black lines. Note that each of these lines follows exactly one color. Moreover, the zones between two neighboring arguments \(\varphi_1=k\cdot\frac{2\pi}{9}\) and \(\varphi_2=(k+1)\cdot\frac{2\pi}{9}\) with \(k\in\mathbb{Z}\) are shaded in a way that the brightness of the colors inside one such zone increases with increasing \(\varphi\), i.e. in counterclockwise sense of rotation.
The other lines visible in the figure above relate to the modulus \(r\). One such line follows the same value of \(r\); it is thus obvious that each of these iso-modulus lines must form a concentric circle on the complex number plane (see the figure above). The distance between neighboring iso-modulus lines is chosen so that it always indicates the same relative change. For reasons to talk about later (see also Wegert 2012), the default setting of the function phasePortrait is a relative change of \(b=\mathrm{e}^{2\pi/9}\) which is very close to \(2\). Thus, with a grain of salt, the modulus of the complex numbers doubles or halves when moving from one iso-modulus line to the other. In the phase portrait, the zones between two adjacent iso-modulus lines are shaded in a way that the colors inside such a zone become brighter in the direction of increasing modulus. The lines themselves are located at the moduli \(r=b^k\), with \(k\in\mathbb{Z}\). This is nicely visible in the phase portrait above, where the outmost circular iso-modulus line indicates (approximately, as \(b\) is not exactly \(2\)) \(r=2^3=8\). Moving inwards, the following iso-modulus lines are at (approximately) \(r=2^2=4\), \(r=2^1=2\), \(r=2^0=1\), \(r=2^{-1}=\frac{1}{2}\), \(r=2^{-2}=\frac{1}{4}\), etc. Obviously, as the modulus of the numbers on the complex plane is their distance from the origin, the width of the concentric rings formed by adjacent iso-modulus lines approximately doubles from ring to ring when moving outwards.
When working with the function phasePortrait, it might not always be desirable to display all of these reference lines and zonings. The argument pType
allows for four different options as illustrated in the next example:
# divide graphics device into four regions and adjust plot margins
<- par(mfrow = c(2, 2),
op mar = c(0.25, 0.55, 1.10, 0.25))
# plot four phase portraits with different choices of pType
phasePortrait("z", xlim = c(-8.5, 8.5), ylim = c(-8.5, 8.5), pType = "p",
main = "pType = 'p'", axes = FALSE, nCores = 2)
phasePortrait("z", xlim = c(-8.5, 8.5), ylim = c(-8.5, 8.5), pType = "pa",
main = "pType = 'pa'", axes = FALSE, nCores = 2)
phasePortrait("z", xlim = c(-8.5, 8.5), ylim = c(-8.5, 8.5), pType = "pm",
main = "pType = 'pm'", axes = FALSE, nCores = 2)
phasePortrait("z", xlim = c(-8.5, 8.5), ylim = c(-8.5, 8.5), pType = "pma",
main = "pType = 'pma'", axes = FALSE, nCores = 2)
par(op) # reset the graphics parameters to their previous values
As evident from the figure above, setting ptype
to ‘p’ displays a phase portrait in the literal sense, i.e. only the phase of the complex numbers is displayed and nothing else. The option ‘pa’ adds reference lines for the argument, the option ‘pm’ adds iso-modulus lines, and the (default) option ‘pma’ adds both. In addition to these options, the example shows phasePortrait in combination with R’s base graphics. The first and the last line of the code chunk set and reset global graphics parameters, and inside the calls to phasePortrait, we use the arguments main
(diagram title) and axes
which are generic plot arguments.
For demonstrating options to adjust the density of the argument and modulus reference lines, consider the rational function \[
f(z)=\frac{(3+2\mathrm{i}+z)(-5+5\mathrm{i}+z)}{(-2-2\mathrm{i}+z)^2}
\] Evidently, this function has two zeroes, \(z_1=-3-2\mathrm{i}\), and \(z_2=5-5\mathrm{i}\). It also has a second order pole at \(z_3=2+2\mathrm{i}\). We make a phase portrait of this function over the same cutout of the complex plane as we did in the figures above. When calling phasePortrait with such simple functions, it is most convenient to define them as as a quoted character string in R syntax containing the variable \(z\). Run the code below for displaying the phase portrait (active 7” x 7” screen graphics device suggested, e.g. x11()
).
<- par(mar = c(5.1, 4.1, 2.1, 2.1), cex = 0.8) # adjust plot margins
op # and general text size
phasePortrait("(3+2i+z)*(-5+5i+z)/(-2-2i+z)^2",
xlim = c(-8.5, 8.5), ylim = c(-8.5, 8.5),
xlab = "real", ylab = "imaginary",
nCores = 2) # Increase or leave out for higher performance
par(op) # reset the graphics parameters to their previous values
The resulting figure nicely displays the function’s two zeroes and the pole. Note that all colors meet in zeroes and poles. Around zeroes, the colors cycle counterclockwise in the order red, green, blue, while this order is reversed around poles. For \(n\)th order (\(n\in\mathbb{N}\)) zeroes and poles, the cycle is passed through \(n\) times. I recommend to check this out with examples of your own.
Now, suppose we want to change the density of the reference lines for the phase angle \(\varphi\). This can be done by way of the argument pi2Div
. For usual applications, pi2Div
should be a natural number \(n\:(n\in\mathbb{N})\). It defines the angle \(\Delta\varphi\) between two adjacent reference lines as a fraction of the round angle, i.e. \(\Delta\varphi=\frac{2\pi}{n}\). The default value of pi2Div
is 9, i.e. \(\Delta\varphi=\frac{2\pi}{9}=40°\). Let’s plot our function in three flavors of pi2Div
, namely, 6, 9 (the default), and 18, resulting in \(\Delta\varphi\) values of \(\frac{\pi}{3}=60°\), \(\frac{2\pi}{9}=40°\), and \(\frac{\pi}{9}=20°\). In order to suppress the iso-modulus lines and display the argument reference lines only, we are using pType = "pa"
. Visualize this by running the code below (active 7” x 2.8” screen graphics device suggested, e.g. x11(width = 7, height = 2.8)
).
# divide graphics device into three regions and adjust plot margins
<- par(mfrow = c(1, 3), mar = c(0.2, 0.2, 0.4, 0.2))
op for(n in c(6, 9, 18)) {
phasePortrait("(3+2i+z)*(-5+5i+z)/(-2-2i+z)^2", xlim = c(-8.5, 8.5), ylim = c(-8.5, 8.5),
pi2Div = n, pType = "pa", axes = FALSE, nCores = 2)
# separate title call (R base graphics) for nicer line adjustment, just cosmetics
title(paste("pi2Div =", n), line = -1.2)
}par(op) # reset graphics parameters to previous values
So far, this is exactly, what had to be expected. But see what happens when we choose the default pType
, "pma"
which also adds modulus reference lines:
# divide graphics device into three regions and adjust plot margins
<- par(mfrow = c(1, 3), mar = c(0.2, 0.2, 0.4, 0.2))
op for(n in c(6, 9, 18)) {
phasePortrait("(3+2i+z)*(-5+5i+z)/(-2-2i+z)^2", xlim = c(-8.5, 8.5), ylim = c(-8.5, 8.5),
pi2Div = n, pType = "pma", axes = FALSE, nCores = 2)
# separate title call (R base graphics) for nicer line adjustment, just cosmetics
title(paste("pi2Div =", n), line = -1.2)
}par(op) # reset graphics parameters to previous values
Evidently, the choice of pi2Div
has influenced the density of the iso-modulus lines. This is because, by default, the parameter logBase
, which controls how dense the iso-modulus lines are arranged, is linked to pi2Div
. As stated above, pi2Div
is usually a natural number \(n\:(n \in\mathbb{N})\), and logBase
is the real number \(b\:(b\in\mathbb{R})\) which defines the moduli \(r=b^k\:(k\in\mathbb{Z})\) where the reference lines are drawn. When \(n\) is given, the default definition of \(b\) is \(b=\mathrm{e}^{2\pi/n}\). In the default case, \(n=9\), this results in \(b\approx2.009994\). Thus, by default, moving from one iso-modulus line to the adjacent one means almost exactly doubling or halving the modulus, depending on the direction. For the other two cases \(n=6\) and \(n=18\), the resulting values for \(b\) are \(b\approx2.85\) and \(b\approx1.42\), the latter obviously being the square root of \(\mathrm{e}^{2\pi/9}\). For \(n=9\), the modulus (approximately) doubles or halves when traversing two adjacent iso-modulus lines.
Before we demonstrate the special property of this linkage between \(n\) and \(b\), i.e. between pi2Div
and logBase
, we shortly show, that they can be decoupled in phasePortrait without any complication. In the following example, we want to define the density of the iso-modulus lines in a way that the modulus triples when traversing three zones in the direction of ascending moduli. Clearly, this requires to define logBase
as \(b=\sqrt[3]{3}\approx1.44\). Thus, when moving from one iso-modulus line to the next higher one, the modulus has increased by a factor of about \(1.4\). One line further, it has about doubled (\({\sqrt[3]{3}}^{2}\approx2.08\)), and another line further it has exactly tripled. While varying pi2Div
exactly as in the previous example, we now keep logBase
constant at \(\sqrt[3]{3}\). Run the code below for visualizing this (active 7” x 2.8” screen graphics device suggested, e.g. x11(width = 7, height = 2.8)
).
# divide graphics device into three regions and adjust plot margins
<- par(mfrow = c(1, 3), mar = c(0.2, 0.2, 0.4, 0.2))
op for(n in c(6, 9, 18)) {
phasePortrait("(3+2i+z)*(-5+5i+z)/(-2-2i+z)^2", xlim = c(-8.5, 8.5), ylim = c(-8.5, 8.5),
pi2Div = n, logBase = sqrt(3), pType = "pma", axes = FALSE, nCores = 2)
# separate title call (R base graphics) for nicer line adjustment, just cosmetics
title(paste("pi2Div = ", n, ", logBase = 3^(1/3)", sep = ""), line = -1.2)
}par(op) # reset graphics parameters to previous values
In order to understand why by default the parameters pi2Div
and logBase
are linked as described above, we consider the exponential function \(f(z)=\mathrm{e}^z\). We can write \(z=r\cdot(\cos\varphi+\mathrm{i}\cdot\sin\varphi)\) and thus \(f(z)=\mathrm{e}^{r\cdot(\cos\varphi+\mathrm{i}\cdot\sin\varphi)}\) or \(w=f(z)=\mathrm{e}^{r\cdot\cos\varphi}\cdot\mathrm{e}^{\mathrm{i}\cdot r\cdot\sin\varphi}\). The modulus of \(w\) is \(\mathrm{e}^{r\cdot\cos\varphi}\) and its argument is \(r\cdot\sin\varphi\) with \(\Re(z)=r\cdot\cos\varphi\) and \(\Im(z)=r\cdot \sin\varphi\). So, the modulus and the argument of \(w=\mathrm{e}^z\) depend solely on the real and the imaginary part of \(z\), respectively. This can be easily verified with a phase portrait of \(f(z)=\mathrm{e}^z\). Run the code below for displaying the phase portrait (active 7” x 7” screen graphics device suggested, e.g. x11()
). Note that in the call to phasePortrait we hand over the exp
function directly as an object. Alternatively, the quoted strings "exp(z)"
or "exp"
can be used as well (see section ways to provide functions to phasePortrait below).
<- par(mar = c(5.1, 4.1, 2.1, 2.1), cex = 0.8) # adjust plot margins
op # and general text size
phasePortrait(exp, xlim = c(-8.5, 8.5), ylim = c(-8.5, 8.5), pType = "pm",
xlab = "real", ylab = "imaginary", nCores = 2)
par(op) # reset graphics parameters to previous values
If we now define the argument pi2Div
as a number \(n\:(n\in\mathbb{N})\) and use it for determining the angular difference \(\Delta\varphi=\frac{2\pi}{n}\) between two subsequent phase angle reference lines, our default link between pi2Div
and logBase
(which is the ratio \(b\) of the moduli at two subsequent iso-modulus lines) establishes \(b=\mathrm{e}^{\Delta\varphi}\). This means, if we add \(\Delta\varphi\) to the argument of any \(w=\mathrm{e}^z\:(z\in\mathbb{C})\) or increase its modulus by the factor \(\mathrm{e}^{\Delta\varphi}\), both are equidistant reference line steps in a plot of \(f(z)=\mathrm{e}^z\). You can visualize this with the following R code (active 7” x 2.8” screen graphics device suggested, e.g. x11(width = 7, height = 2.8)
):
# divide graphics device into three regions and adjust plot margins
<- par(mfrow = c(1, 3), mar = c(0.2, 0.2, 0.4, 0.2))
op for(n in c(6, 9, 18)) {
phasePortrait("exp(z)", xlim = c(-8.5, 8.5), ylim = c(-8.5, 8.5),
pi2Div = n, pType = "pma", axes = FALSE, nCores = 2)
# separate title call (R base graphics) for nicer line adjustment, just cosmetics
title(paste("pi2Div = ", n, ", logBase = exp(2*pi/pi2Div)", sep = ""),
line = -1.2, cex.main = 0.9)
}par(op) # reset graphics parameters to previous values
As expected, the default coupling of both arguments produces square patterns when applied to a phase portrait of the exponential function which can, insofar, serve as a visual reference. Recall, that equidistant modulus reference lines (in ascending order) indicate an exponentially growing modulus. In the middle phase portrait one such steps means (approximately) doubling the modulus. From the left to the right, the plot covers 24 of these steps, indicating a total increase of the modulus by factor \(2^{24}\) which amounts to almost 17 millions.
For optimizing visualization in a technical sense, as well as for aesthetic purposes, it may be useful to adjust shading and contrast of the argument and modulus reference zones mentioned above. This is done by modifying the parameters darkestShade
(\(s\)) and lambda
(\(\lambda\)) when calling phasePortrait
. These two parameters can be used to steer the transition from the lower to the uper edge of a reference zone. They address the v-value of the hsv color model, which can take values between 0 and 1, indicating maximum darkness (black), and no shading at all, respectively. Hereby, \(s\) gives the v-value at the lower edge of a reference zone, and \(\lambda\) determines the interpolation from there to the upper edge, where no shading is applied. The intended use is \(\lambda > 0\) where small values sharpen the contrast between shaded and non-shaded zones and vice versa. Exactly, the shading value \(v\) is calculated as: \[
v = s + (1-s)\cdot x^{1/\lambda}
\] For modulus zone shading at a point \(z\) in the complex plane when portraying a function \(f(z)\), \(x\) is the fractional part of \(\log_b{f(z)}\), with the base \(b\) being the parameter logBase
that defines the modulus reference zoning (see above). For shading argument reference zones, \(x\) is simply the difference between the upper and the lower angle of an argument reference zone, linearly mapped to the range \([0, 1[\). The following code generates a \(3\times3\) matrix of phase portraits of \(f(z)=\tan{z^2}\) with \(\lambda\) and \(s\) changing along the rows and columns, respectively. Run the code for visualizing these concepts (active 7” x 7” screen graphics device suggested, e.g. x11()
).
<- par(mfrow = c(3, 3), mar = c(0.2, 0.2, 0.2, 0.2))
op for(lb in c(0.1, 1, 10)) {
for(dS in c(0, 0.2, 0.4)) {
phasePortrait("tan(z^2)", xlim = c(-1.7, 1.7), ylim = c(-1.7, 1.7),
pType = "pm", darkestShade = dS, lambda = lb,
axes = FALSE, xaxs = "i", yaxs = "i", nCores = 2)
}
}par(op)
Additional possibilities exist for tuning the interplay of modulus and argument reference zones when they are used in combination; this can be controlled with the parameter gamma
when calling phasePortrait
). The maximum brightness of the colours in a phase portrait is adjustable with the parameter stdSaturation
(see documentation of phasePortrait
; we will also get back to these points in the chapter aesthetic hints below).
When exploring functions with phasePortrait, discontinuities of certain functions can become visible as abrupt color changes. Typical examples are integer root functions which, for a given point \(z, z\in\mathbb{C}\setminus\lbrace0\rbrace\) in the complex plane, can attain \(n\) values with \(n\) being the root’s degree. It takes, so to speak, \(n\) full cycles around the origin of the complex plane in order to cover all values obtained from a function \(f(z)=z^{1/n}, n\in\mathbb{N}\). The code below creates an illustration comprising three phase portraits with branch cuts (dashed lines), illustrating the three values of \(f(z)=z^{1/3}\), \(z\in\mathbb{C}\setminus\lbrace0\rbrace\). The transitions between the phase portraits are indicated by same-coloured arrows pointing at the branch cuts. For running the code, an open 7” x 2.7” graphics device is suggested, e.g. x11(width = 7, height = 2.8)
.
<- par(mfrow = c(1, 3), mar = c(0.4, 0.2, 0.2, 0.2))
op for(k in 0:2) {
<- paste0("z^(1/3) * exp(1i * 2*pi/3 * ", k, ")")
FUNstring phasePortrait(FUN = FUNstring,
xlim = c(-1.5, 1.5), ylim = c(-1.5, 1.5), pi2Div = 12,
axes = FALSE, nCores = 2)
title(sub = paste0("k = ", k), line = -1)
# emphasize branch cut with a dashed line segment
segments(-1.5, 0, 0, 0, lwd = 2, lty = "dashed")
# draw colored arrows
<- switch(as.character(k),
upperCol "0" = "black", "1" = "red", "2" = "green")
<- switch(as.character(k),
lowerCol "0" = "green", "1" = "black", "2" = "red")
arrows(x0 = c(-1.2), y0 = c(1, -1), y1 = c(0.2, -0.2),
lwd = 2, length = 0.1, col = c(upperCol, lowerCol))
}par(op)
After you have run the code, have a look at the leftmost diagram first. Note that the argument reference lines have been adjusted to represent angle distances of \(30°\), i.e. pi2Div
= 12. Most noticeable is the abrupt color change from yellow to magenta along the negative real axis (emphasized with a dashed line). This is what is called a branch cut, and it suggests that our picture of the function \(f(z)=z^{1/3}\) is not complete. As the three third roots of any complex number \(z=r\cdot\mathrm{e}^{\mathrm{i}\varphi}, z\in\mathbb{C}\setminus\lbrace0\rbrace\) are \(r^{1/3}\cdot\mathrm{e}^{\mathrm{i}\cdot(\varphi+k\cdot2\pi)/3}; k=0,1,2; \varphi\in\left[0,2\pi\right[\), we require three different phase portraits, one for each \(k\), as shown in the figure above. With the argument reference line distance being \(30°\), it is easy to see that each phase portrait covers a total argument range of \(120°\), i.e. \(2\pi/3\).
Obviously, each of the three portraits has a branch cut along the negative real axis, and the colors at the branch cuts show, where the transitions between the phase portraits have to happen. In the figure, we have illustrated this by arrows pointing to the branch cuts. Same-colored arrows in different phase portraits indicate the transitions. Thus, the first phase portrait (\(k = 0\)) links to the second (\(k = 1\)) in their yellow zones (black arrows); the second links to the third (\(k = 2\)) in their blue zones (red arrows), and the third links back to the first in their magenta zones (green arrows). Actually, one could imagine to stack the three face portraits in ascending order, cut them at the dashed line, and glue the branch cuts together according to the correct transitions. The resulting object is a Riemann surface with each phase portrait being a ‘sheet.’ See more about this fascinating concept in Wegert (2012), Chapter7.
While the function \(f(z)=z^{1/3}\) could be fully covered with three phase portraits, \(f(z)=\log z\) has an infinite number of branches. As the (natural) logarithm of any complex number \(z=r\cdot\mathrm{e}^{i\cdot\varphi}, r>0\) is \(\log z=\log r+\mathrm{i}\cdot\varphi\), it is evident that the imaginary part of \(\log z\) increases linearly with the argument of \(z\), \(\varphi\). In terms of phase portraits, this means an infinite number of stacked ‘sheets’ in either direction, clockwise and counterclockwise. Neighboring sheets connect at a branch cut. Run the code below to illustrate this with a phase portrait of \(\log z=\log r+\mathrm{i}\cdot(\varphi+k\cdot2\pi), r > 0, \varphi\in\left[0,2\pi\right[\) for \(k=-1, 0, 1\) (active 7” x 2.7” screen graphics device suggested, e.g. x11(width = 7, height = 2.7)
). In the resulting illustration, the branch cuts are marked with dashed white lines.
<- par(mfrow = c(1, 3), mar = c(0.4, 0.2, 0.2, 0.2))
op for(k in -1:1) {
<- paste0("log(Mod(z)) + 1i * (Arg(z) + 2 * pi * ", k, ")")
FUNstring phasePortrait(FUN = FUNstring, pi2Div = 36,
xlim = c(-2, 2), ylim = c(-2, 2), axes = FALSE, nCores = 2)
segments(-2, 0, 0, 0, col = "white", lwd = 1, lty = "dashed")
title(sub = paste0("k = ", k), line = -1)
} par(op)
A convenient way to visualize the whole complex number plane is based on a stereographic projection suggested by Bernhard Riemann (see Wegert (2012), p. 20 ff. and p. 39 ff.). The Riemann Sphere is a sphere with radius 1, centered around the origin of the complex plane. It is cut into an upper (northern) and lower (southern) half by the complex plane. By connecting any point on the complex plane to the north pole with a straight line, the line’s intersection with the sphere’s surface marks the location on the sphere where the point is projected onto. Thus, all points inside the unit disk on the complex plane are projected onto the southern hemisphere, the origin being represented by the south pole. In contrast, all points outside the unit disk are projected onto the northern hemisphere, the north pole representing the point at infinity. For visualizing both hemispheres as 2D phase portraits, they have to be projected onto a flat surface in turn.
If we perform a stereographic projection of the southern hemisphere from the north pole to the complex plane (and look at the plane’s upper - the northern - side), this obviously results in a phase portrait on the untransformed complex plane as were all examples shown so far in this text. We can perform an analogue procedure for the northern hemisphere, projecting it from the south pole to the complex plane. We now want to think of the northern hemisphere projection as layered on top of the southern hemisphere projection, for the northern hemisphere, which it depicts, is naturally also on top of the southern hemisphere. If, in a ‘normal’ visualization of the complex plane (orthogonal real and imaginary axes), a point at any location represents a complex number \(z\), a point at the same location in the northern hemisphere projection is mapped into \(1/z\). The origin is mapped into the point at infinity. Technically, this mapping can be easily achieved when calling the function phasePortrait
by setting the flag invertFlip = TRUE
(default is FALSE
). The resulting map is, in addition, rotated counter-clockwise around the point at infinity by an angle of \(\pi\). As Wegert (2012) argues, this way of mapping has a convenient visual effect: Consider two phase portraits of the same function, one made with invertFlip = FALSE
and the other one with invertFlip = TRUE
. Both are shown side by side (see the pairs of phase portraits in the next two figures below). This can be imagined as a view into a Riemann sphere that has been cut open along the equator and swung open along a hinge in the line \(\Re(z)=1\) (if the southern hemisphere is at the left side) or \(\Re(z)=-1\) (if the northern hemisphere is at the left side). In order to highlight the Riemann sphere in Phase Portraits if desired, we provide the function riemannMask
. Let’s first demonstrate this for the function \(f(z)=z\).
<- par(mfrow = c(1, 2), mar = rep(0.1, 4))
op # Southern hemisphere
phasePortrait("z", xlim = c(-1.4, 1.4), ylim = c(-1.4, 1.4),
pi2Div = 12, axes = FALSE, nCores = 2)
riemannMask(annotSouth = TRUE)
# Northern hemisphere
phasePortrait("z", xlim = c(-1.4, 1.4), ylim = c(-1.4, 1.4),
pi2Div = 12, axes = FALSE, invertFlip = TRUE, nCores = 2)
riemannMask(annotNorth = TRUE)
par(op)
The function riemannMask
provides several options, among others adjusting the mask’s transparency or adding annotations to landmark points (see the function’s documentation). In the next example, we will use it without any such features. Consider the following function: \[
f(z)=\frac{(z^{2}+\frac{1}{\sqrt{2}}+\frac{\mathrm{i}}{\sqrt{2}})\cdot(z+\frac{1}{2}+\frac{\mathrm{i}}{2})}{z-1}
\] This function has two zeroes exactly located on the unit circle, \(z_1=\mathrm{e}^{\mathrm{i}\frac{5\pi}{8}}\), and \(z_2=\mathrm{e}^{\mathrm{i}\frac{13\pi}{8}}\). Moreover, it has another zero inside the unit circle, \(z_3=\frac{1}{\sqrt{2}}\cdot\mathrm{e}^{\mathrm{i}\frac{5\pi}{4}}\). Equally obvious, it has a pole exactly on the unit circle, \(z_4=1\). Less obvious, it has a double pole, \(z_5\), at the point at infinity. The code required for producing the following figure looks somewhat bulky, but most lines are required for annotating the zeroes and poles. Note that the real axis coordinates of the northern hemisphere’s annotation do not have to be multiplied with \(-1\) in order to take into account the rotation of the inverted complex plane. By calling phasePortrait
with invertFlip = TRUE
the coordinate system of the plot is already set up correctly and will remain so for subsequent operations.
<- par(mfrow = c(1, 2), mar = c(0.1, 0.1, 1.4, 0.1))
op # Define function
<- "(z^2 + 1/sqrt(2) * (1 + 1i)) * (z + 1/2*(1 + 1i)) / (z - 1)"
FUNstring # Southern hemisphere
phasePortrait(FUNstring, xlim = c(-1.2, 1.2), ylim = c(-1.2, 1.2),
pi2Div = 12, axes = FALSE, nCores = 2)
riemannMask()
title("Southern Hemisphere", line = 0)
# - annotate zeroes and poles
text(c(cos(5/8*pi), cos(13/8*pi), cos(5/4*pi)/sqrt(2), 1),
c(sin(5/8*pi), sin(13/8*pi), sin(5/4*pi)/sqrt(2), 0),
c(expression(z[1]), expression(z[2]), expression(z[3]), expression(z[4])),
pos = c(1, 2, 4, 2), offset = 1, col = "white")
# Northern hemisphere
phasePortrait(FUNstring, xlim = c(-1.2, 1.2), ylim = c(-1.2, 1.2),
pi2Div = 12, axes = FALSE, invertFlip = TRUE, nCores = 2)
riemannMask()
title("Northern Hemisphere", line = 0)
# - annotate zeroes and poles
text(c(cos(5/8*pi), cos(13/8*pi), cos(5/4*pi)*sqrt(2), 1, 0),
c(sin(5/8*pi), sin(13/8*pi), sin(5/4*pi)*sqrt(2), 0, 0),
c(expression(z[1]), expression(z[2]), expression(z[3]),
expression(z[4]), expression(z[5])),
pos = c(1, 4, 3, 4, 4), offset = 1,
col = c("white", "white", "black", "white", "white"))
par(op)
With some consideration it becomes quite easy to see that both phase portraits are kind of everted versions of each other. What is inside the unit disk in the left phase portrait is outside in the right one, and vice versa. If you mentally visualize both unit disks touching in, e.g., point \(z_4\) and one disk rolling along the edge of the other, you will see immediately how one disk continues the picture shown in the other. Having the ‘Riemann Mask’ somewhat transparent is helpful for orientation (see the function’s documentation). Note, how the zeroes \(z_{1, 2}\) and the pole \(z_4\) which are located exactly on the unit circle continue outside the unit disk on ‘their own’ plane and in the other unit disk. Note also, that on both hemispheres zeroes and poles can be identified by the same sequence of colors: When circling counter-clockwise around the point of interest, a zero will always exhibit the color sequence red, yellow, green, blue, magenta, red …, while this order will always be reverted for a pole. As the pole at the point at infinity (\(z_5\)) is a double pole, the color sequence is run through twice during one turn around the pole. As the zero \(z_3\) is inside the unit disk representing the southern hemisphere, it lies outside the northern hemisphere disk, but it is still visible on the continuation of the inverted (and rotated) complex plane (belonging to the northern hemisphere) outside the unit disk. Observe, how the grid lines in the vicinity of \(z_3\) merge when passing from one unit disk to the other.
While visualizing fractals is not among the original purposes of this package, phasePortrait allows for an unusual way of displaying such that are functions of complex numbers. Classic examples are the Mandelbrot set and the Julia set, and this package provides the functions mandelbrot
and juliaNormal
(implemented in C++) for supporting visualization. The mandelbrot set comprises all complex numbers \(z\) for which the sequence \(a_{n+1}=a_n^{2}+z\) with \(a_0=0\) remains bounded for all \(n\in\mathbb{N_0}\). Normal julia sets are closely related to the Mandelbrot set. They comprise all complex numbers \(z\) for which the sequence \(a_{n+1}=a_n^2+c\) with \(a_0=z\) remains bounded for all \(n\in\mathbb{N_0}\). The parameter \(c\) is a complex number, and interesting visualizations with this package (i.e. other than a blank screen) are only obtained for \(c\) being an element of the Mandelbrot set. For the author’s taste, the Julia set visualizations look best and are most interesting when \(c\) is located near the border of the Mandelbrot set. The classic visualizations of the Mandelbrot and the Julia set use a uniform color (usually black) for all points which belong to the set and color the points outside the set dependent on how quickly they diverge. Visualizations with phasePortrait, in contrast, color the points inside the sets by the argument and modulus of the number to which the series described above converges (or, more precisely, at which the iteration terminates). While the results are visually appealing (please try the code examples we provide below), they are not unambiguous, as the sequences that define the sets do not always converge to one single value, but to limit cycles.
The following code plots an overview picture of the Mandelbrot set. Note that the function mandelbrot
is called with a considerably low value (30) for the parameter itDepth
which defines the number of iterations to be calculated (default is 500). This is because we are using phasePortrait for plotting into a graphics window with a comparatively low resolution (see section defining image quality for how to obtain high-quality phase portraits). While a high number of iterations produces a more accurate representation of the set, the resulting filigree structures might become hardly visible or even invisible when the resolution to be plotted on is low.
x11(width = 8, height = 2/3 * 8) # Open graphics window on screen
<- par(mar = c(0, 0, 0, 0)) # Do not leave plot margins
op phasePortrait(mandelbrot, moreArgs = list(itDepth = 30),
ncores = 1, # Increase or leave out for higher performance
xlim = c(-2, 1), ylim = c(-1, 1),
hsvNaN = c(0, 0, 0), # black color for points outside the set
axes = FALSE, # No coordinate axes
xaxs = "i", yaxs = "i") # No space between plot region and plot
par(op) # Set graphics parameters to original
With the code example below we plot a cutout of the Mandelbrot set into a png file with a resolution of 600 dpi using the default number of iterations (500). We are using a few features that are just commented here, but will be explained below in the section aesthetic hints. Other graphics file formats can be used in almost the same way. Type ?png
in order to see all formats and how to call them. See also section defining image quality.
<- 600 # set resolution to 600 dpi
res # open png graphics device with in DIN A4 format
# DIN A format has an edge length ratio of sqrt(2)
png("Mandelbrot Example.png",
width = 29.7, height = 29.7/sqrt(2), # DIN A4 landscape
units = "cm",
res = res) # resolution is required
<- par(mar = c(0, 0, 0, 0)) # set graphics parameters - no plot margins
op <- c(-1.254, -1.248) # horizontal (real) plot limits
xlim # the function below adjusts the imaginary plot limits to the
# desired ratio (sqrt(2)) centered around the desired imaginary value
<- ylimFromXlim(xlim, centerY = 0.02, x_to_y = sqrt(2))
ylim phasePortrait(mandelbrot,
nCores = 1, # Increase or leave out for higher performance
xlim = xlim, ylim = ylim,
hsvNaN = c(0, 0, 0), # Black color for NaN results
xaxs = "i", yaxs = "i", # suppress R's default axis margins
axes = FALSE, # do not plot axes
res = res) # resolution is required
par(op) # reset graphics parameters
dev.off() # close graphics device and complete the png file
Inside the same technical setting, the following two examples plot Julia sets into a png file.
<- 600
res png("Julia Example 1.png", width = 29.7, height = 29.7/sqrt(2),
units = "cm", res = res)
<- par(mar = c(0, 0, 0, 0))
op <- c(-1.8, 1.8)
xlim <- ylimFromXlim(xlim, centerY = 0, x_to_y = sqrt(2))
ylim phasePortrait(juliaNormal,
# see documentation of juliaNormal about the arguments
# c and R_esc
moreArgs = list(c = -0.09 - 0.649i, R_esc = 2),
nCores = 1, # Increase or leave out for higher performance
xlim = xlim, ylim = ylim,
hsvNaN = c(0, 0, 0),
xaxs = "i", yaxs = "i",
axes = FALSE,
res = res)
par(op)
dev.off()
<- 600
res png("Julia Example 2.png", width = 29.7, height = 29.7/sqrt(2),
units = "cm", res = res)
<- par(mar = c(0, 0, 0, 0))
op <- c(-0.32, 0.02)
xlim <- ylimFromXlim(xlim, center = -0.78, x_to_y = sqrt(2))
ylim phasePortrait(juliaNormal,
# see documentation of juliaNormal about the arguments
# c and R_esc
moreArgs = list(c = -0.119 - 0.882i, R_esc = 2),
nCores = 1, # Increase or leave out for higher performance
xlim = xlim, ylim = ylim,
hsvNaN = c(0, 0, 0),
xaxs = "i", yaxs = "i",
axes = FALSE,
res = res)
par(op)
dev.off()
Since version 1.1.0, viscomplexr provides the function phasePortraitBw
which allows for creating two-color phase portraits of complex functions based on a polar chessboard grid (cf. Wegert (2012), p. 35). Compared to the full phase portraits that can be made with phasePortrait
, two-color portraits omit information. Especially in combination with full phase portraits they can be, however, very helpful tools for interpretation. Besides, two-color phase portraits have a special aesthetic appeal which is worth exploring for itself. In its parameters and its mode of operation, phasePortraitBw
is very similar to phasePortrait
(see the documentations of both functions for details). The parameters pi2Div
and logBase
have exactly the same effect as with phasePortrait
. Instead of the parameter pType
, phasePortraitBw
has the parameter bwType
which allows for the three settings “m,” “a,” and “ma.” These produce two-color phase portraits which take into account the modulus only, the argument (phase angle), only, and the combination of both, respectively. Plots made with the latter option show a chessboard-like color alteration over the tiles resulting from the intersection of modulus and argument zones. The following code maps the complex plane to itself, comparing all three options of bwType
. It also adds a standard phase portrait for comparison.
# Map the complex plane on itself, show all bwType options
x11(width = 8, height = 8)
<- par(mfrow = c(2, 2), mar = c(4.1, 4.1, 1.1, 1.1))
op for(bwType in c("ma", "a", "m")) {
phasePortraitBw("z", xlim = c(-2, 2), ylim = c(-2, 2),
bwType = bwType,
xlab = "real", ylab = "imaginary",
nCores = 2) # Increase or leave out for higher performance
}# Add normal phase portrait for comparison
phasePortrait("z", xlim = c(-2, 2), ylim = c(-2, 2),
xlab = "real", ylab = "imaginary",
pi2Div = 18, # Use same angular division as default
# in phasePortraitBw
nCores = 2) # Increase or leave out for higher performance
par(op)
Note that the parameter pi2Div
should not be chosen as an odd number when working with phasePortraitBw
. In this case, the first and the last phase angle zone would obtain the same color, which is probably an undesired effect for most applications. While pi2Div = 9
is the default setting in phasePortrait
for good reasons (see above), its default in phasePortraitBw
is 18. Also by default, the parameter logBase
is linked to pi2Div
in the same way as by default in phasePortrait
(logBase = exp(2*pi/pi2Div)
). So, if defaults for both, phasePortrait
and phasePortraitBw
are used, each zone of the former covers two zones of the latter. In the code above, however, pi2Div
was set to 18 also in the call to phasePortrait
for direct comparability. This is also the case in the code below, which displays a rational function.
# A rational function, show all bwType options
x11(width = 8, height = 8)
<- "(z + 1.4i - 1.4)^2/(z^3 + 2)"
funString <- par(mfrow = c(2, 2), mar = c(4.1, 4.1, 1.1, 1.1))
op for(bwType in c("ma", "a", "m")) {
phasePortraitBw(funString, xlim = c(-2, 2), ylim = c(-2, 2),
bwType = bwType,
xlab = "real", ylab = "imaginary",
nCores = 2) # Increase or leave out for higher performance
}# Add normal phase portrait for comparison
phasePortrait(funString, xlim = c(-2, 2), ylim = c(-2, 2),
xlab = "real", ylab = "imaginary",
pi2Div = 18, # Use same angular division as default
# in phasePortraitBw
nCores = 2) # Increase or leave out for higher performance
par(op)
While the letters ‘Bw’ in phasePortraitBw
stand for ‘black/white,’ the natural colors for such chessboard plots, the user is not limited to these. The choice of colors is defined by the parameter bwCols
which, by default, is set as bwCols = c("black", "gray95", "gray")
. The first and the second color are used for coloring the alternating zones, while the last color is used in cases where the function of interest produces results which cannot be sufficiently evaluated for modulus or argument (NaN
, partly Inf
). Note that the second color, "gray95"
is almost, but not exactly white, which contrasts ‘white’ tiles against a white background in a visually unobtrusive way. The parameter bwCols
can be freely changed; values must be either color names that R can interpret (call colors()
for a list) or hexadecimal color strings like e.g. "#00FF32"
(the format is "#RRGGBB"
with ‘RR,’ ‘GG,’ and ‘BB’ representing red, green, and blue with an allowed value range of 00
to FF
for each).
While phase portraits were originally invented for scientific and technical purposes, their aesthetic quality is a feature in itself. In this section, we give a few technical hints that might be helpful for obtaining appealing graphics. We will be not only talking about features implemented in this package, but also mention some useful options provided by R base graphics. A general recommendation when plotting for maximum aesthetic results is also to first check out the function to be plotted with lower resolutions (e.g. the default 150 dpi), and a smaller format (but with the desired device aspect ratio), adjust the domain (xlim
, ylim
), and all other parameters of phasePortrait you might want to change (including the parameters gamma
and stdSaturation
we have not mentioned in this vignette, so far). Only if you are satisfied with the result at that stage, you should start the run with the desired final resolution and format, as plots at high resolution and large formats may be time-consuming depending on your hardware (see also the section defining image quality). When using the function riemannMask
you could try changing the mask’s transparency (alphaMask
) and it’s color (colMask
). Often, black is a good alternative to the default white). Besides such simple issues there are, however, a few points we will talk about in more detail below.
par(op)
mechanismAs mentioned above, phasePortrait uses R’s base graphic system. This is a powerful tool, its functionality is, however, not always easy to understand and use. Many fundamental settings of base R graphics are stored in a set of parameters, which can be set or queried using the function par()
. Among the important graphical parameters in our context are those which steer the outer margins and the plot margins (oma
, omi
, mar
, mai
) and those which define the default background and foreground colors (bg
, fg
). Type ?par
to see a documentation of all parameters. Changing these parameters here and there during an R session can easily lead to graphical results that may be nice but hard to reproduce. For avoiding this, when par()
is called to change one or more graphical parameters, it invisibly returns all parameters and their values before the change. These can be stored in a variable, and used to restore the original parameter values after the plotting has been done. This concept seems to be unknown to surprisingly many users of R:
# Set the plot margins at all four sides to 1/5 inch with mai,
# set the background color to black with bg, and the default foreground
# color with fg (e.g. for axes and boxes around plots, or the color of
# the circle outline from the function riemannMask).
# We catch the previous parameter values in a variable, I called
# "op" ("old parameters")
<- par(mai = c(1/5, 1/5, 1/5, 1/5), bg = "black", fg = "white")
op
# Make any phase portraits and/or other graphics of your interest
# ...
# Set the graphical parameters back to the values previously stored in op
par(op)
Usually, when aiming for mainly aesthetic effects, you want to suppress plot axes from being drawn. As phase phasePortrait accepts, via its ...
argument, all arguments also accepted by R’s plot.default
, this can be easily achieved by providing the argument axes = FALSE
:
phasePortrait("tan(z^3 + 1/2 - 2i)/(1 - 1i - z)",
xlim = c(-6, 6), ylim = c(-3, 3),
axes = FALSE,
nCores = 2) # Increase or leave out for higher performance
Note that this does not only suppress both axes, but also the box usually drawn around a plot. If such a box is desired, it can be simply added afterwards by calling box()
:
phasePortrait("tan(z^3 + 1/2 - 2i)/(1 - 1i - z)",
xlim = c(-6, 6), ylim = c(-3, 3),
axes = FALSE,
nCores = 2) # Increase or leave out for higher performance
box()
If axes are desired together with a special aesthetic appeal (e.g. for presentations), it is worth trying out a black background and white axes. However, there are unexpected hurdles to take, before the result looks as it should:
# set background and foreground colors
<- par(bg = "black", fg = "white")
op # Setting the parameter fg has an effect on the box, the axes, and the axes'
# ticks, but not on the axis annotations and axis labels.
# Also the color of the title (main) is not affected.
# The colors of these elements have to be set manually and separately. While we
# could simply set them to "white", we set them, more flexibly, to the
# current foreground color (par("fg")).
phasePortrait("tan(z^3 + 1/2 - 2i)/(2 - 1i - z)",
xlim = c(-6, 6), ylim = c(-3, 3), col.axis = par("fg"),
xlab = "real", ylab = "imaginary", col.lab = par("fg"),
main = "All annotation in foreground color", col.main = par("fg"),
# Adjust text size
cex.axis = 0.9, cex.lab = 0.9,
nCores = 2) # Increase or leave out for higher performance
par(op)
Note that by default the axes are constructed with an overhang of 4% beyond the ranges given with xlim
and ylim
at each end. More often than not this looks nice, but sometimes it is undesired, e.g. when a phase portrait is intended to cover the full display without any frame and margin. This behavior is due to the graphical parameters xaxs
and yaxs
(axis style) being set to ‘r’ (‘regular’) by default. Setting these parameters as xaxs = "i"
and yaxs = "i"
(‘internal’), no overhang is added. Both, xaxs
and yaxs
, can be either set in a call to par()
or handed as arguments to phasePortrait. We will come back to these parameters in the following section.
You might want to plot a phase portrait that fully covers the graphics device. The following code example shows how to achieve this. First, it is necessary to set the plot margins to zero (note that the outer margins are zero by default, so, usually, there is no need to care for them). Second, as phasePortrait uses an aspect ratio of 1 by default, xlim
and ylim
have to exactly match the aspect ratio of the graphics device to be plotted in. In order to facilitate this, we provide the functions ylimFromXlim
and xlimFromYlim
. In the example, we use the former in order to match xlim
and ylim
a device aspect ratio of 16/9. Third, in order to omit the 4% axis overhang (would look like a margin), the parameters xaxs
and yaxs
are set to “i.” Setting axes
to FALSE is not absolutely necessary in this case, but is good style.
# Open graphics device with 16/9 aspect ratio and 7 inch width
x11(width = 7, height = 9/16 * 7)
<- par(mar = c(0, 0, 0, 0)) # Set plot margins to zero
op <- c(-3, 3)
xlim # Calculate ylim with desired center fitting the desired aspect ratio
<- ylimFromXlim(xlim, centerY = 0, x_to_y = 16/9)
ylim phasePortrait(jacobiTheta, moreArgs = list(tau = 1i/5 + 1/5), pType = "p",
xlim = xlim, ylim = ylim,
xaxs = "i", yaxs = "i",
axes = FALSE,
nCores = 2) # Increase or leave out for higher performance
par(op)
Not many changes are necessary for obtaining a phase portrait like above but with a frame. A convenient way to do this is to set the outer margins of the graphics device in inches with the graphical parameter omi
and the background to the desired color. Adding this to the code above, however, leads to differing horizontal and vertical frame widths. This occurs because, due to the margin setting, the required ratio of the xlim
and ylim
ranges is no longer exactly 16/9. The precise ratio has to be calculated and provided to ylimFromXlim
as shown in the code example below.
# Open graphics device with 16/9 aspect ratio and a width of 7 inches
x11(width = 7, height = 9/16 * 7)
# Set plot margins to zero, outer margins to 1/7 inch,
# and background color to black
<- 1/7 # outer margin width in inches
outerMar <- par(mar = c(0, 0, 0, 0), omi = rep(outerMar, 4), bg = "black")
op <- c(-1.5, 0.5)
xlim # Calculate ylim with desired center fitting the desired aspect ratio;
# however, the omi settings slightly change the required
# ratio of xlim and ylim
<- (7 - 2*outerMar) / (7 * 9/16 - 2*outerMar)
ratio <- ylimFromXlim(xlim, centerY = 0, x_to_y = ratio)
ylim phasePortrait("sin(jacobiTheta(z, tau))/z", moreArgs = list(tau = 1i/5 + 1/5),
pType = "p",
xlim = xlim, ylim = ylim,
xaxs = "i", yaxs = "i",
axes = FALSE,
nCores = 1) # Increase or leave out for higher performance
par(op)
This chapter details a few technical points which might be of interest for optimizing the results obtained by using the R package at hand. We talk about different ways to provide functions to phasePortrait, and about how to control image quality. And there is more: The function phasePortrait has to perform several memory and time critical operations. In order to keep memory utilization on a reasonable level and to optimize computing times, the function works with temporary files and parallel processing. We explain both below, because the user can influence their behavior. For avoiding unnecessary copying of big arrays, phasePortrait makes also use of pointers, but as there is no related control option for the user, we reserve this for a later version of this vignette.
Any function to be visualized with phasePortrait must be provided as the argument FUN
. In some cases (see below), the argument moreArgs
can turn out useful in combination with FUN
. Probably the easiest way of defining FUN
is a character string which is an expression R can evaluate as a function of a complex number \(z\). See some examples:
# Note that 'FUN =' is not required if the argument to FUN is handed to
# phasePortrait in the first position
phasePortrait(FUN = "1/(1 - z^2)", xlim = c(-5, 5), ylim = c(-5, 5), nCores = 2)
phasePortrait("sin((z - 2)/(z + 2))", xlim = c(-5, 5), ylim = c(-5, 5), nCores = 2)
phasePortrait("tan(z)", xlim = c(-5, 5), ylim = c(-5, 5), nCores = 2)
If your expression requires arguments besides \(z\) you can provide them to phasePortrait by means of moreArgs
, which expects a named list containing the additional arguments:
phasePortrait("-1 * sum(z^c(-k:k))", moreArgs = list(k = 11),
xlim = c(-2, 2), ylim = c(-1.5, 1.5),
pType = "p",
nCores = 2) # Increase or leave out for higher performance
While we recommend other solutions (see below), it is also possible to hand over more extensive user-defined functions as character strings. To make this work, however, the function must be wrapped in a vapply
construct which guarantees the output of the function being a complex number by setting vapply’s argument FUN.VALUE
as complex(1)
. Moreover, the first argument to vapply
must be \(z\). In such cases, it is often convenient to define the character string outside the call to phasePortrait and hand it over after that, as we do in the following example:
<- "vapply(z, FUN = function(z) {
funString n <- 9
k <- z^(c(1:n))
rslt <- sum(sin(k))
return(rslt)
},
FUN.VALUE = complex(1))"
phasePortrait(funString, xlim = c(-2, 2), ylim = c(-2, 2),
nCores = 2) # Increase or leave out for higher performance
If such a function has arguments in addition to \(z\), they can be included into the call to ‘vapply’ and thus included into the string (for supporting this, we provide the function vector2string
, see the example in its documentation), but we do not recommend that. Anyway, if you must know, here it is:
<- "vapply(z, FUN = function(z, fct) {
funString n <- 9
k <- z^(fct * c(1:n))
rslt <- sum(sin(k))
return(rslt)
},
fct = -1,
FUN.VALUE = complex(1))"
phasePortrait(funString, xlim = c(-2, 2), ylim = c(-2, 2),
nCores = 2) # Increase or leave out for higher performance
Probably, the most useful application of this concept is when the vapply
construct is pasted together at runtime with values for the additional arguments depending on what happened earlier. However, defining the function directly as a function first, and then simply passing its name to phasePortrait leads to a more readable code:
# Define function
<- function(z, fct, n) {
tryThisOne <- z^(fct * c(1:n))
k <- prod(cos(k))
rslt return(rslt)
}
# Call function by its name only, provide additional arguments via "moreArgs"
phasePortrait("tryThisOne", moreArgs = list(fct = 1, n = 5),
xlim = c(-2.5, 2.5), ylim = c(-2, 2),
nCores = 2) # Increase or leave out for higher performance
As the function in the example above requires two additional arguments beside \(z\), we hand them over to phasePortrait via the argument moreArgs
, which must be (even in case of only one additional argument) a named list (names must match the names of the required arguments), where the argument values are assigned.
Besides character strings as shown above, the argument FUN
can also directly take function objects. The simplest case is an anonymous function definition:
# Use argument "hsvNaN = c(0, 0, 0)" if you want the grey area black
phasePortrait(function(z) {
for(j in 1:20) {
<- z * sin(z) - 1 + 1/2i
z
}return(z)
},xlim = c(-3, 3), ylim = c(-2, 2),
nCores = 2) # Increase or leave out for higher performance
Evidently, this can be used with moreArgs
as well:
# Use argument "hsvNaN = c(0, 0, 0)" if you want the grey area black
phasePortrait(function(z, n) {
for(j in 1:n) {
<- z * cos(z)
z
}return(z)
},moreArgs = list(n = 27),
xlim = c(-3, 3), ylim = c(-2, 2),
nCores = 2) # Increase or leave out for higher performance
Any function object that is known by name to R, be it user-defined or contained in a package will work in the same way. Just hand over the function object itself:
# atan from package base
phasePortrait(atan, xlim = c(-pi, pi), ylim = c(-pi, pi),
nCores = 2)
# gammaz from package pracma (the package must be installed on your machine
# if you want this example to be working)
phasePortrait(pracma::gammaz, xlim = c(-9, 9), ylim = c(-5, 5),
nCores = 2)
# blaschkeProd from this package (moreArgs example)
# make random vector of zeroes
<- 12
n <- complex(modulus = runif(n), argument = 2 * pi * runif(n))
a # plot the actual phase portrait
phasePortrait(blaschkeProd, moreArgs = list(a = a),
xlim = c(-1.3, 1.3), ylim = c(-1.3, 1.3),
nCores = 2)
# User function example
<- function(z, n, r) {
tryThisOneToo for(j in 1:n) {
<- r * (z + z^2)
z
} return(z)
} # Use argument "hsvNaN = c(0, 0, 0)" if you want the gray areas black
phasePortrait(tryThisOneToo, moreArgs = list(n = 50, r = 1/2 - 1/2i),
xlim = c(-3, 2), ylim = c(-2.5, 2.5),
nCores = 2)
Defining own functions in C++ and using them with phasePortrait usually gives a substantial performance gain. This is especially true if they require operations that cannot be vectorized in R (i.e. if there is now way to avoid for-loops or similar). The ideal tool for integrating C++ code in R programs is Rcpp (Eddelbuettel and Balamuta 2017), but be aware that C++ functions compiled with Rcpp::sourceCpp
will not work with phasePortrait, as this is not compatible with parallel processing. What it takes is to provide the C++ functions as or as part of an R package which is not difficult at all. The functions of the math
family included in this package have all been coded in C++ and integrated with Rcpp.
Clearly, there is a trade-off between the quality of an image plotted with phasePortrait and the computing time. The image quality is defined by the argument res
and has a default value of 150 dpi. For pictures in standard sizes of about 30 x 20 cm plotting with 150 dpi does not take much time, and for many purposes, this resolution is sufficient. When resolutions of 300, 600 or more dpi are desired for high-quality printouts, we recommend to try out everything with 150 dpi (and, maybe, on a small format) before starting the final high-quality run. Technically, early after being called, phasePortrait gets the plot region size of the active graphics device and calculates the number of required pixels from this size and the value of res
. Note, that R graphics devices for files, like png
, bmp
, jpeg
and tiff
, also expect a parameter res
(if the units given for device height and width are cm or inches). For plotting to graphics files, we suggest to store the desired resolution in a variable first, and pass it to both, the graphics device and phasePortrait:
<- 300 # Define desired resolution in dpi
res png("Logistic_Function.png", width = 40, height = 40 * 3/4,
units = "cm", res = res)
phasePortrait("1/(1+exp(-z))", xlim = c(-25, 25), ylim = c(-15, 15), res = res,
xlab = "real", ylab = "imaginary",
nCores = 2) # Increase or leave out for higher performance
dev.off()
In order to keep the machine’s RAM workload manageable, phasePortrait will always save data in temporary files. These files are stored in the directory specified with the argument tempDir
(default is the current R session’s temporary directory). After normal execution, these files will be automatically deleted, so, usually, there is no need to care about. Automatic deletion, however, will not happen, if the user calls phasePortrait with the parameter deleteTempFiles
set to FALSE or if phasePortrait does not terminate properly. Thus, if phasePortrait crashed you should check the directory specified as tempDir
and delete these files, because they usually are of considerable size. However, such orphans will never interfere with further runs of phasePortrait (see below).
The size of these temporary files depends from phasePortrait’s parameter blockSizePx
(default: 2250000; it may be worth varying this value in order to obtain optimum performance on your machine). If the two-dimensional array of pixels to be plotted comprises more pixels than specified by this parameter, the array will be vertically split into blocks of that size. These sub-arrays are what is stored in the temporary files. More precisely, there are two temporary files per sub-array. One represents the cutout of the untransformed complex plane over which the function of interest is applied; the other contains the values obtained by applying the function to the first one. Thus, each array cell contains a double precision complex number; in a temporary file pair an array cell at the same position refers to the same pixel in the plot.
These files are .RData
files, and their names adhere to a strict convention, see the following examples:
0001zmat2238046385.RData
0001wmat2238046385.RData
These are examples of names of a pair of temporary files belonging to the same block (sub-array). The names are equal except for one substring which can either be ‘zmat’ or ‘wmat.’ The former file contains an untransformed cutout of the complex plane, and the latter the corresponding values obtained from the function of interest as explained above.
Both names begin with ‘0001,’ indicating that the array’s top line is the first line of the whole pixel array to be covered by the phasePortrait. The name of the file that contains the subsequent array can e.g. begin with a number like ‘0470,’ indicating that its first line is line number 470 of the whole array. The number of digits for these line numbers is not fixed. It is determined by the greatest number required. Numbers with less digits are zero-padded. The second part of the file name is either zmat or wmat (see above). The third part of the file names is a ten-digit integer. This is a random number which all temporary files stemming from the same call of phasePortrait have in common. This guarantees that no temporary files will be confounded in subsequent calls of phasePortrait, even if undeleted temporary files from previous runs are still present.
For enhanced performance, phasePortrait (and in the same way phasePortraitBw) uses parallel processing as provided via the R packages doParallel
(Microsoft and Weston 2019) and foreach
(Microsoft and Weston 2020). The number of processor cores to be used can be set with the parameter nCores
when calling phasePortrait. By default, one less than all available cores will be utilized. Clearly, setting nCores = 1
will result in sequential processing. When phasePortrait is called with ncores > 1
, and no parallel backend is registered, one will be registered first. The same applies, when a parallel backend is already registered, but the user desires a different number of cores. This registering may take some time. Therefore, when it terminates, phasePortrait does not automatically de-register the parallel backend (and register a sequential backend again). This saves registering time in subsequent runs of phasePortrait with the same number of cores to be used. This default behavior - keeping parallel backends registered after termination - can be changed by setting the parameter autoDereg
on TRUE (default is FALSE). Otherwise, phasePortrait, after completing the plot, prints a message that the current parallel backend can be manually de-registered with the command foreach::registerDoSEQ()
. We recommend to do this after the last call to phasePortrait in an R session.
There are three occasions, when phasePortrait utilizes parallel processing: First, after determining the size and number range (in the complex plane) of the whole two dimensional array of pixels to be plotted, the sub-arrays (blocks) corresponding to the parameter value blockSizePx
are constructed and saved as temporary files in a parallel loop. These are the temporary files with the string zmat
in their names (see section Temporary files). Second, while the single blocks are loaded and processed sequentially, each block is evaluated in a separate parallel process. In order to do so, the block is split into a few approximately equally sized parts; the number of these parts corresponds to the number of processor cores to be used. In each parallel process the function to be plotted is applied to each single cell of the corresponding block part (internally vectorized with vapply
). The outcomes of all parallel processes are combined into one array which is saved as a temporary file which has the string wmat
in its name. Third, for transforming the function values stored in the wmat
files into hsv colors, a similar concept as in the second step is utilized: The single wmat
files are processed sequentially, but the array stored in each file is split into chunks which are dealt with parallely. Eventually, transforming all wmat
arrays results in one large color value array which can be plotted after that. Handling this large array is the most memory-intensive task when running phasePortrait, and it can take considerable time for large plots in high quality. So far, however, no alternative solution provided fully satisfying results.
As mentioned in the section about temporary files, users can possibly optimize performance by trying different values for the parameter blockSizePx
. We mention this here as well, as blockSizePx
does not only influence size and number of the temporary files, but also the size of the array chunks that are processed parallely.
Obviously, applying the function of interest to millions of values is time-critical. Therefore, when defining a function for a phase portrait in R, use all options at hand for vectorizing calculations. Moreover, you can count on a significant performance gain when you write time critical functions in C++. Thanks to the package Rcpp (Eddelbuettel and Balamuta 2017) this not really a hurdle anymore. As mentioned above, however, C++ functions compiled with Rcpp::sourceCpp
will not work with phasePortrait, as this is not compatible with parallel processing; you have to provide such functions in a package. The package at hand provides a few functions (function family maths
); all of them have been implemented in C++.
While this package is a leisure project, it would have been a mission impossible without the background of my daily work with R as a Forest Scientist at the Technical University of Munich (TUM). Fortunately, I have a job that allows me to learn about Nature by asking her questions (or trying to simulate what she is doing) with ever-improving methods and tools. I would like to thank everyone at the Chair of Forest Growth and Yield Science at TUM who keep me involved in discussions like: How can this be solved in R …
switch(1 + trunc(runif(1, 0, 6)),
"... at all?",
"... in a quick-and-dirty way?",
"... in Hadley-Wickham-style?",
"... without a loop?",
"... without nested loops?",
"... in a way somebody can understand?")
Veronika Biber provided expert advice for improving the vignette. Johannes Biber turned out the most patient pre-release tester one can imagine, boosting things with his high-end gaming machine. Thanks, guys! Also thanks to Gregor Seyer for his helpful review of the CRAN submission.
Clearly, programming in R would not be what it is, weren’t there some R titans who generously share their knowledge online. While I keep learning from all of them, I would like to thank especially Hadley Wickham and Dirk Eddelbüttel.