Domain coloring is a much more complicated plotting technique than those outlined in the plotting chapter and is used to plot complex functions where both the input and output have imaginary and real components. For the code in this chapter, we will focus on languages that are easily able to plot two-dimensional images or heat maps, instead of languages meant for number-crunching. That is to say that this chapter will certainly have a code implementation in gnuplot, but it will not likely have an implementation in C, Fortran, or Java because these languages do not have plotting capabilities in-built.
To start, imagine the following function: . In this case, we could create a plot that looks like this:
This indicates that for various input values along , we have different function outputs from . For this function, is purely in real space and because of this, the output is also in real space. Now let's imagine another function with complex input , but a purely real output :
In this case, this can be plotted as a two-dimensional dataset like so:
Here, the -axis and -axis represent the imaginary and real components of the input variable, respectively. The color bar represents the output of .
At this point, we can start to see the problem. If the output of also requires plotting of real and imaginary components, then we would need four dimensions to appropriately represent the full function space, one axis for the real component and another for the imaginary component of both the input () and the output of ! Unfortunately, feeble human minds are incapable of understanding four spatial dimensions without projecting onto lower dimensionality, so we need to improvise.
We do this by assuming the complex output can be represented in the following form:
where, is a complex magnitude and is a complex phase. This is the formula for a circle in the complex plane and we can easily find and like so:
Once we have our complex function output in this form, we then color the output domain according to a color space with at least 2 independent dimensions, like RGB (Red, Green, Blue), or HSV (Hue, Saturation, Value) . The choice of color space is completely dependent on what the users feel is most visually intuitive. In any case, one dimension of the color system will be used to represent the complex magnitude and another dimension of the color system will be used to represent the complex phase of the output. The grid will be representing the real and imaginary inputs to these functions. That is to say, we plug every value in the 2D complex plane into the function and then color each pixel based on the function output.
As an example, let's look at the simplest function we can , but in this case . If we use an RGB color scheme, where red represents and blue represents , we can generate the following image:
As a note here, there is a clear phase discontinuity along the horizontal axis, which is a consequence of the fact that the complex phase wraps around the origin, ranging from 0 (clear) to (red). In addition, the edges of the plot are blue because the function's magnitude increases linearly as we move from the origin.
If we instead look at the function , we can generate a similar plot:
Here, it is clear that the complex phase wraps around the origin twice, creating two separate phase discontinuities on top of each other. This indicates a phase winding. For some purposes, such as vortex tracking for inviscid fluids, this visualization is ideal, because a vortex is located precisely at the center of the phase discontinuity  . For other purposes, the discontinuity is visually distracting, and for this reason, many people use an HSV scheme for plotting complex functions   . So here is the same function , but using hue to represent the complex phase and saturation to represent the magnitude:
In this plot, the Value for HSV was always set to 1. When looking at the edges of the plot, the hue changes rapidly, but each color is mirrored on the opposite edge. This indicates the phase winding we saw in the RGB plot. Also, because the complex magnitude increases as we move further from the center of the plot, the saturation also increases. Thus the center of the plot is completely washed out! We need to fix this in subsequent plots to make them more representative of the actual data.
One easy way to show the increasing complex magnitude without sacrificing phase information is by using contours. Essentially, at ever integer value of the magnitude, we want to draw some kind of line. There are a number of ways to generate these lines, and one simple way is by using an alternative shading function like so:
This will create the following image:
This function will essentially create a smooth gradient, but because of the floor operation , the saturation will go from 0 to 1 between each integer value of the magnitude. Here, it is clear that the magnitude is increasing as from the origin; however, because the saturation is fluctuating so much, it is difficult to see the phase pattern next to each contour. This can be fixed simply by adding an offset to the shading function such that,
Which will produce the following image:
This means that the saturation will fluctuate from to 1 instead of from 0 to 1, which makes it way easier to see phase information next to contours. Again, there are a lot of different ways to play with these equations, so feel free to use whatever function you want! As long as some sort of rounding operation is used to establish some form of integer value for the magnitude, it should be possible to create contours of various types.
At this point, changing the saturation shows changes in the complex magnitude, and changing the hue shows changes in the complex phase. Unfortunately, neither the magnitude nor the phase directly show what is happening in real or imaginary space with the output. To show this, we might want to draw grid lines that color our pixels black whenever the imaginary or real components of the output function are integer values.
For example, let's go back to a simpler function . If we draw lines on this plot, corresponding to integer values in the output, we get a simple grid
Like before, the choice of which function to use in order to create the grid lines is somewhat arbitrary. It is important to choose a function that sharply drops to 0 or peaks at 1 for all integer values, and then we simply plug values of into this function. For the purposes of this chapter, we chose the following function
where is some threshold value, and was set to be 0.1 in our plot. A plot of for where is shown below:
So, putting it all together and returning to the function of , we find the following image.
Here, the diagonal lines through the center represent integer values along the imaginary axis for and the vertical and horizontal lines represent integer values of the real axis for . An easy way to determine which lines correspond to which integer values is by plugging in certain values for into . For example, there is a black line at where , this means that all values along that contour correspond to values that are constrained to having an imaginary component of precisely 2.
Overall, there are plenty of interesting ways to plot complex functions and make really compelling and beautiful images! We will be using domain coloring in other contexts throughout this text when describing methods that heavily use complex space.
Here is a video describing domain coloring:
Here is the full script to generate a domain colored output of .
# setting output to file of size 800 x 800 set terminal pngcairo size 1000, 1000 set output 'domain.png' # sets title for full plot set title 'f(z)=z^2' # removes legend unset key # projects image onto 2D plane set view map # sets aspect ratio of plot to be square set size square # sets x and y range and labels set xrange[-2:2] set yrange[-2:2] set xlabel "Re(z)" set ylabel "Im(z)" # scaling the x, y, and colorbar tics to zero so they are not seen in the plot set xtics border scale 0,0 set ytics border scale 0,0 set cbtics border scale 0,0 # sets tics in color bar at 0 and 2pi set cbtics ("0" -3.14159, '2pi' 3.14159) set cblabel "Phase Angle" set cbrange [ -3.14159 : 3.14159 ] # use hsv for colorbar and set palette to use full hsv space set palette model HSV set palette defined ( 0 0 1 1, 1 1 1 1 ) # setting isosamples for output grid and samples for input grid set isosamples 2000, 2000 set samples 2000, 2000 # setting functions necessary for domain coloring # setting threshold for gridlines. Smaller threshold will make smaller lines thresh = 0.1 f(z) = z**2 # atan2 returns a range from -pi to pi, so we need to add pi, but this offsets # the value by 180 degrees, so we also imput (-y, -x) for another 180 degrees # to invert rotation angle(x,y) = (pi + atan2(-y,-x)) / (2*pi) # complex magnitude r(x,y) = sqrt(x*x + y*y) # complex phase and magnitude theta(x,y) = atan2(y,x) z(x,y) = r(x,y)*exp(theta(x,y)*sqrt(-1)) # imaginary and real output functions imaginary_f(z) = imag(f(z)) real_f(z) = real(f(z)) # magnitude contours magnitude_shading(x,y) = 0.5 + 0.5*(abs(f(z(x,y)))-floor(abs(f(z(x,y))))) # gridlines gridlines(x,y) = (abs(sin(real_f(z(x,y))*pi)**thresh) \ * abs(sin(imaginary_f(z(x,y))*pi))**thresh) # overall coloring function color(x,y) = hsv2rgb(angle(real_f(z(x,y)), imaginary_f(z(x,y))), \ magnitude_shading(x,y), \ gridlines(x,y)) save_encoding = "utf8" # Last datafile plotted: "++" # In this case, it means, "plot the data file created with the # samples and isosamples" splot '++' using 1:2:(color($1,$2)) with pm3d lc rgb variable nocontour
import numpy as np import matplotlib.pyplot as plt import matplotlib.colors from matplotlib.cm import ScalarMappable def f(z): return z**2 def magnitude_shading(f_val): f_val_abs = np.abs(f_val) return 0.5 + 0.5 * (f_val_abs - np.floor(f_val_abs)) def gridlines(f_val, threshold): return (np.abs(np.sin(np.pi * np.real(f_val))) ** threshold * np.abs(np.sin(np.pi * np.imag(f_val))) ** threshold) def color(f_val, threshold): hue = (np.pi - np.angle(f_val)) / (2.0 * np.pi) saturation = magnitude_shading(f_val) value = gridlines(f_val, threshold) # Currently we have a tuple of 2D-arrays (hue, saturation, value). # This makes it a 2D-array of tuples, which the conversion function requires. hsv = np.moveaxis((hue, saturation, value), 0, -1) return matplotlib.colors.hsv_to_rgb(hsv) if __name__ == "__main__": # Create a new figure containing a single plot fig, axes = plt.subplots(1, 1) # Set the title for the plot axes.set_title("$f(x)=z^2$") # Create color bar cbar = fig.colorbar( ScalarMappable(matplotlib.colors.Normalize(0.0, 2.0 * np.pi), "hsv"), ax=axes, label="Phase Angle") # Set x and y labels axes.set_xlabel("$Re(z)$") axes.set_ylabel("$Im(z)$") # Set color bar tick locations and labels cbar.set_ticks([0.0, np.pi, 2.0 * np.pi]) cbar.set_ticklabels(["$0.0$", "$\pi$", "$2\pi$"]) # Hide x and y ticks for tick in axes.get_xticklines(): tick.set_visible(False) for tick in axes.get_yticklines(): tick.set_visible(False) # Create a 500x500 input grid coords = np.linspace(-2.0, 2.0, 500) z_real, z_imag = np.meshgrid(coords, coords) z = z_real + 1j * z_imag # Calculate function values f_val = f(z) # Map function values to colors colors = color(f_val, 0.1) # Plot the colors # extent=(-2.0, 2.0, -2.0, 2.0) sets the x and y ranges # origin="lower" places index (0,0) of the color array in the lower-left corner # aspect="equal" ensures that the plot is square axes.imshow( colors, extent=(-2.0, 2.0, -2.0, 2.0), origin="lower", aspect="equal") # Save output fig.savefig("domain.png")
|1||Wikipedia: HSL and HSV, 2020.|
|2||Schloss, James, Massively parallel split-step Fourier techniques for simulating quantum systems on graphics processing units, 2019.|
|3||Pethick, Christopher J and Smith, Henrik, Bose--Einstein condensation in dilute gases, Cambridge university press, 2008.|
|4||Wegert, Elias, Visual complex functions: an introduction with phase portraits, Springer Science \& Business Media, 2012.|
|5||Poelke, Konstantin and Polthier, Konrad, Domain Coloring of Complex Functions,|
|6||Lundmark, Hans, Visualizing complex analytic functions using domain coloring, 2004.|
The code examples are licensed under the MIT license (found in LICENSE.md).
The text of this chapter was written by James Schloss and is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License.
- The image "z2" was created by James Schloss and is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License.
- The image "absz" was created by James Schloss and is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License.
- The image "rgb1" was created by James Schloss and is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License.
- The image "rgb2" was created by James Schloss and is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License.
- The image "hsv1" was created by James Schloss and is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License.
- The image "hsv2" was created by James Schloss and is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License.
- The image "hsv3" was created by James Schloss and is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License.
- The image "hsv4" was created by James Schloss and is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License.
- The image "shade" was created by James Schloss and is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License.
- The image "hsv5" was created by James Schloss and is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License.
The following pull requests have modified the text or graphics of this chapter: