The hardware and bandwidth for this mirror is donated by METANET, the Webhosting and Full Service-Cloud Provider.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]metanet.ch.
“To deal with hyper-planes in a 14-dimensional space, visualize a 3-D space and say ‘fourteen’ to yourself very loudly. Everyone does it.”
— Geoffrey Hinton (Lecture 2, Slide 16 from his 2012 Coursera course Neural Networks for Machine Learning)
The hardest part of using Modal Color Theory is the high number of dimensions that it needs to describe most scales. A scale with \(n\) notes lives in a space of \(n-1\) dimensions. (The \(-1\) comes from the fact that we consider scales to be transpositionally equivalent, so you can assume that \(\hat{1}\) of every scale is 0.) This means that pentachords, the smallest collection that we’re likely to really consider to be scale-like, need to be understood in four dimensions–already not easy to visualize–and it only gets worse from there.
Fortunately, R can help us visualize the scale-space geometries two or three dimensions at a time. This won’t be a picture of the whole space all at once, but it’s a lot better than nothing. This vignette models using musicMCT to depict a few specific cross-sections of the hyperplane arrangements.
We’re going to explore the specific but important case of heptachords, using a strategy inspired by the old practice of building scales by combining two tetrachords. This practice derives from ancient Greek music theory and forms an important part of the theory of Arabic maqamat. I should stress, though, that this vignette is inspired by those theories, not an attempt to explain or reformulate them. I’m not an expert in either subject, and a maqam is more than just an ordered collection of fixed pitches. Still, I do suspect that tetrachordal thinking is useful at least in part for the way that it simplifies the range of possibilities that a musician must consider when using a scale. That simplification is a point of commonality between the visualization we’re about to do and the actual musical practices of Ancient Greek and Arabic classical musics (to the extent I understand them). I’d like to think that this commonality could be enough to allow MCT’s mathematical model to enrich our understanding of these theories, but for this vignette our focus is only the technical problem of simplifying high-dimensional spaces.
Our initial approach will be to restrict attention to seven-note scales that can be built from two tetrachords according to the following plan:
Transpositional combination of this sort is easy to perform in
musicMCT with the function tc()
. Let’s see how the normal
12-equal major scale can be derived as the transpositional combination
of an (0, 2, 4, 5) tetrachord at a perfect fifth:
This structure is useful because it fixes most of the pitches in the scale, allowing only the values of the middle two notes in the lower tetrachord to vary freely. (The middle pitches of the upper tetrachord must match the lower one, so they are completely determined by our decisions about scale degrees 2 and 3.) This means that all scales with this structure live in a two-dimensional subspace of the overall heptachord geometry: the two dimensions of the subspace correspond directly to our choices for the values of scale degree 2 and 3. Let’s define a function that takes in a pair of choices for the variable scale degrees (i.e. a “genus” in Ancient Greek theory) and returns an entire scale:
scale_from_genus <- function(genus) {
sd2 <- genus[1]
sd3 <- genus[2]
if (sd2 > sd3) {
return(NA)
}
tc(c(0, sd2, sd3, 5), c(0, 7))
}
Since the space of such scales is only two dimensional, it will be
easy to visualize on a normal x-y plot. The space is a planar slice
through the complete 6-D heptachord space. For concision, I’ll call it
the “tc()
plane” since we can build any scale in it with
tc()
.
To understand the tc()
plane’s relationship to the whole
heptachord space, recall that every “hue” in MCT is a ray that emanates
outward from the perfectly even scale at the center of the space (in
this case, seven-tone equal temperament). The perfectly even heptachord
cannot be constructed by scale_from_genus()
, so it does not
lie on the tc()
plane. Therefore, each point in our
visualization will be an intersection between some hue and our
tc()
plane. You might think of our visualization as a
window pane that we’re looking through: each spot on the pane actually
corresponds to a line of sight in our field of view. Figure 1 sketches
this schematically, but remember that the actual tc()
plane
is embedded in a 6-D space rather than the normal 3-D world:
Figure 1: Schematic representation of the tc() plane in heptachord space
Figure 1 is not a precise picture of heptachord space, but as a step
toward interpreting our actual visualizations, we might imagine that the
ray labeled “hue 1” represents the hue of meantone major scales. Another
way to say this is that “hue 1” represents all the heptachords that have
Carey and Clampitt’s property of “well formedness” and can be
represented as the pattern of steps LLSLLLS where L and S represent
large and small steps. As long as L > S and the pattern of 5 large
and 2 small steps adds up to a full octave, any choice of specific
values for L and S will result in a scale that lies along this hue. The
point where hue #1 intersects the tc()
plane will be
specifically the scale where L=2 and S=1, which is to say exactly the
familiar major scale from 12-tone equal temperament. (We know that the
intersection between the hue and the tc()
plane must be
this specific scale, because our tc()
construction
guarantees that the interval of disjunction between the two tetrachords
is exactly 2 semitones, which forces our hand with regard to all the
other L and S step sizes.)
Similarly, we might imagine that “hue 3” represents well-formed
heptachords with the step pattern SLLLSLL, which is to say all tunings
of the phrygian octave species. The visible point on both hue 3 and the
tc()
plane is then 12tet phrygian: (0, 1, 3, 5, 7, 8,
10).
Hue 2 has been drawn to lie in between hues 1 and 3: all three hues
lie on a single plane in heptachord space which cuts across the
tc()
plane. The plane containing hues 1, 2, and 3
intersects the tc()
plane in a line, so the three “visible
points” in Figure 1 all lie on a line in the tc()
plane.
The visible point of hue 2 must be some intermediate scale between
ionian and phrygian. If it were exactly half way between those two
diatonic modes, its values would simply an average of ionian and
phrygian: (0, 1.5, 3.5, 5, 7, 8.5, 10.5). This would actually be another
well-formed scale with the step pattern SLSLSLS. But, given the way I’ve
drawn the figure, it looks to me like hue 2 is slightly closer to hue 3
than hue 1, so we might guess that it represents the scale (0, 1.4, 3.4,
5, 7, 8.4, 10.4). This has the step pattern SLMLSLM (where M represents
a medium step size somewhere between the
small and large steps). This is not
coincidentally a pattern that fits David Clampitt’s definition of a
“pairwise well-formed” scale. Such scales are always two-dimensional
subspaces that run from one well-formed hue to another.
If you aren’t familiar with well-formed and pairwise well-formed
scales, don’t worry too much about the preceding paragraph. The
important point is that a scalar subspace which is inherently
two-dimensional (like the plane containing all of hues 1, 2, and 3) will
show up in our visualization as a single line (i.e. the line connecting
the three visible points in Figure 1). More generally, any structure
that looks like it has dimension \(k\)
in the tc()
plane will actually have dimension \(k+1\) in the overall space. Our
visualization is a 2-D plane but it smuggles in a third dimension, which
is the one that travels in and out along the hues in Figure 1. (This
dimension is what MCT calls “saturation”; it can be manipulated with
saturate()
.) You might compare this to looking at a street
map of a city, which is inherently 2-D but where you can easily imagine
an invisible height dimension.
Now that we know what the tc()
plane is and have a sense
of how it relates to the overall heptachord space, let’s start actually
visualizing it. We’ll begin by plotting a few specific scales to use as
reference points. Among scales that have familiar names in Western music
theory, there are several that have the desired tetrachordal
structure:
ionian <- scale_from_genus(c(2, 4)) #1
dorian <- scale_from_genus(c(2, 3)) #2
phrygian <- scale_from_genus(c(1, 3)) #3
double_harmonic <- scale_from_genus(c(1, 4)) #4
enharmonic <- scale_from_genus(c(.5, 1)) #5
demo_scales <- cbind(ionian, dorian, phrygian, double_harmonic, enharmonic)
To see these scales on the plane, we define the function
tetra_plot()
and feed our scales into it.
tetra_plot <- function(scales, title, ...) {
oldpar <- par(bg='aliceblue')
on.exit(par(oldpar))
plot(scales[2, ],
scales[3, ],
xlab="Height of scale degree 2", xlim=c(-.05, 5.05),
ylab="Height of scale degree 3", ylim=c(-.01, 5.01),
...)
grid(col="gray35")
mtext(side=3, title, font=2, line=1)
}
tetra_plot(demo_scales,
"Location of 5 Reference Scales in the tc() Plane",
pch=sapply(1:5, toString))
Figure 2
The scales are marked by numbers in the order that we defined them: the ionian scale is “1”, dorian “2”, and so on. It will be useful to keep these points in mind as landmarks while we populate the space with more scales. (Note, by the way, that ionian is #1 and phrygian is #3 just like they were in Figure 1. This is just a coincidence. In our plot, dorian is #2 and doesn’t lie on a straight line between 1 and 3, so it doesn’t correspond to “hue #2” from Figure 1.)
The tc()
plane has an inversional symmetry that’s just
barely visible in this initial plot, but which becomes more conspicuous
as we add scales. Both scale 2 (dorian, {0, 2, 3, 5, 7, 9, 10}) and
scale 4 (double harmonic, {0, 1, 4, 5, 7, 8, 11}) are symmetrical under
\(T_0 I\). All scales in the
tc()
plane that lie on the northwest-southeast line from #2
to #4 have this same symmetry. Moreover, pairs of scales that are
reflections across that line are transformed into each other by \(T_0 I\). That’s the transformation that
takes C major (at point 1) to C phrygian (at point 3). Similarly, we
could imagine reflecting the scale built from the enharmonic genus at
point 5 across the line of inversional symmetry, which should give a new
scale in the top right quadrant of the graph, with quarter-tone steps
clustered toward the top of its tetrachords. Let’s verify that this
works and plot the new scale as point #6:
# Invert the original enharmonic scale:
inverted_enharmonic <- tni(enharmonic, 0)
# Define a new enharmonic scale based on where we expect to plot it:
new_enharmonic <- scale_from_genus(c(4, 4.5))
# The two are the same:
rbind(inverted_enharmonic, new_enharmonic)
#> [,1] [,2] [,3] [,4] [,5] [,6] [,7]
#> inverted_enharmonic 0 4 4.5 5 7 11 11.5
#> new_enharmonic 0 4 4.5 5 7 11 11.5
# Let's plot them:
demo_scales <- cbind(demo_scales, inverted_enharmonic)
tetra_plot(demo_scales,
"Location of 6 Reference Scales in the tc() Plane",
pch=sapply(1:6, toString))
Figure 3
That definitely starts to make the symmetry of the space more
visually clear. Before we move on, let’s encapsulate the work we’ve done
with a new function, show_landmarks()
, which lets us add
reminders of these six scales to any new plots we create:
Let’s now take a look at the landscape of the whole plane, not just
individual points on it. The simplest question we might ask is “how many
distinct scalar colors can be visualized on the tc()
plane?” We can answer that by randomly sampling many points on the
plane, calculating their sign vectors, and counting how many distinct
sign vectors result. First we’ll do that only as a numeric computation,
sampling 4000 points in the plane:
num_points <- 4000
parhypatai <- runif(num_points, 0, 5)
lichanoi <- runif(num_points, 0, 5)
inputs <- rbind(parhypatai, lichanoi) |> apply(MARGIN=2, FUN=sort)
random_scales <- apply(inputs, 2, scale_from_genus)
all_signvectors <- apply(random_scales, 2, signvector)
unique_signvectors <- all_signvectors |> unique(MARGIN=2) |> apply(MARGIN=2, FUN=toString)
unique_signvectors <- sort(unique_signvectors)
length(unique_signvectors)
#> [1] 26
This tells us that the scales we’ve sampled represent 26 different scalar colors. We can see them by plotting all the scales, coloring each point to reflect which scalar “color” it belongs to. (Of course, the association between each visual color and the scalar structure it represents is arbitrary: there’s nothing essentially more blue or red about one scale structure than another.) Let’s also include the six “landmark” scales from Figure 3, plotted as numbers rather than colored points:
match_sv <- function(sv) {
res <- which(unique_signvectors == toString(sv))
if (length(res)==0) {
return(0)
}
res
}
scalar_colors <- apply(all_signvectors, 2, match_sv)
display_colors <- palette.colors(26, palette="Polychrome 36")[scalar_colors]
tetra_plot(random_scales,
"26 Scalar Colors in the tc() Plane",
col=display_colors,
pch=20)
show_landmarks()
Figure 4
Each colored polygon in this figure represents a different scale structure. For instance, landmark #4 (the “double harmonic” scale) sits at the boundary between two right triangles (reddish purple to its lower left and yellow-green to its upper right). Scales inside those colored polygons have three degrees of freedom to vary their pitches without fundamentally altering their structure: up/down and left/right as visualized and also in/out along their line of saturation. (Recall Figure 1.) All of the scales that Figure 4 plots as colored points have these 3 degrees of freedom: our random sample of scales on the plane didn’t produce any 1- or 2-D scales because it’s very unlikely that a randomly generated scale will have more regularity than what is required by our definition of scale_from_genus(). Nonetheless, we can see the 1- and 2-D scales in Figure 4 as the boundaries between colored polygons. Landmark scale #4 (double harmonic), for instance, is 2-D because it lies directly on the line segment that separates the reddish purple and yellow-green triangles:
Scalar colors with only one degree of freedom show up on the
tc()
plane as points where multiple lines intersect. All
three diatonic modes that we can visualize (landmarks #1, #2, and #3)
are at such intersections, and (like all well-formed scales) that have
only one degree of freedom:
If we wanted to run some calculations on examples of the 1- or 2-D
scales, we would need a way to generate them explicitly. We could think
through defining them with a specific function, along the lines of
scale_from_genus(), but musicMCT also allows us to project a given scale
onto any “flat” (i.e. intersection of hyperplanes) in the arrangement.
If we know the specific hyperplanes that we want to project onto, we can
use project_onto()
; but if we have a specific scale that
already lies on the desired flat, we can use match_flat()
.
To see this latter function in action, let’s project all 4000 sampled
scales onto the same flat as the double harmonic scale: we should end up
with 4000 points all lying on the same northwest-southeast diagonal line
as double harmonic itself. I’ll plot the new projected scales a black
points, in addition to the colorful points from Figure 4.
projected_scales <- apply(random_scales, 2, match_flat, target_scale=double_harmonic)
colors_for_projected_scales <- rep("black", num_points)
scales_for_fig5 <- cbind(random_scales, projected_scales)
colors_for_fig5 <- c(display_colors, colors_for_projected_scales)
tetra_plot(scales_for_fig5,
"Double Harmonic's Flat as a Line of Black Points",
col=colors_for_fig5,
pch=20)
show_landmarks()
Figure 5
Now that we have specific examples of scales on that flat to work
with, we can explore whatever properties we’re interested in. For
instance, Clampitt 2009
tells us that the double_harmonic
scale is “pairwise
well-formed.” (In fact, it’s an example of the unusual class of
singular pairwise-well formed scales.) Do all the scales that
lie on its flat have that property, too?
test_for_pwf <- apply(projected_scales, 2, isgwf)
table(test_for_pwf)
#> test_for_pwf
#> TRUE
#> 4000
All 4000 of our projected_scales
return
TRUE
: they’re all pairwise well-formed! It’s not hard to
show that this is true in general: like well-formedness, pairwise
well-formedness is either always true or always false for the 2-D scales
on a given flat.
Properties that can only be true or false aren’t terribly interesting
to visualize, so let’s also play around with a property that can take
continuous values. For instance, might wonder the scales’
evenness()
changes as we vary along the line. We’ll
calculate the evenness of each scale that lies on the flat and plot the
points so that more even scales are represented by larger points.
evenness_values <- apply(projected_scales, 2, evenness)
sizes_for_fig6 <- c(rep(1, num_points), max(evenness_values)-evenness_values)
tetra_plot(scales_for_fig5,
"Line Thickness Represents Scale Evenness",
col=colors_for_fig5,
pch=20,
cex=sizes_for_fig6)
show_landmarks()
Figure 6
You can see that the largest points (and thus the most even scales)
lie in the zone between landmarks 2 and 4. The scales get very uneven as
we move toward the top left corner of the plot. This makes sense,
because the point (0, 5) at the top left of the tc()
plane
represents a scale whose \(\hat{2}\) is
0 (the same as \(\hat{1}\)) and whose
\(\hat{3}\) is 5 (the same as \(\hat{4}\)): such a scale is very uneven
because its “stepwise motion” varies widely between not moving at all
and making large leaps.
Now, most of the detail in the tc()
plane lies in the
square bounded by landmarks 1, 2, 3, and 4. It’s hard to tell exactly
how many colors lie in that region, and similarly hard to tell exactly
where the thickest point on the diagonal black line is. We can zoom in
to get a better view of that part of the plane:
zoomed_tetra_plot <- function(scales, title, ...) {
oldpar <- par(bg='aliceblue')
on.exit(par(oldpar))
plot(scales[2,],
scales[3, ],
xlab="Height of scale degree 2", xlim=c(0.95, 2.05),
ylab="Height of scale degree 3", ylim=c(2.99, 4.01),
...)
grid(col="gray35")
mtext(side=3, title, font=2, line=1)
}
zoomed_parhypatai <- runif(num_points, 1, 2)
zoomed_lichanoi <- runif(num_points, 3, 4)
zoomed_inputs <- rbind(zoomed_parhypatai, zoomed_lichanoi)
zoomed_sets <- apply(zoomed_inputs, 2, scale_from_genus)
zoomed_signvectors <- apply(zoomed_sets, 2, signvector)
zoomed_scalar_colors <- apply(zoomed_signvectors, 2, match_sv)
zoomed_display_colors <- palette.colors(26, palette="Polychrome 36")[zoomed_scalar_colors]
points_for_fig7 <- cbind(zoomed_sets, projected_scales)
colors_for_fig7 <- c(zoomed_display_colors, colors_for_projected_scales)
sizes_for_fig7 <-c(rep(1, num_points), 3^(evenness(double_harmonic)-evenness_values))
zoomed_tetra_plot(points_for_fig7,
"Scalar Colors in the tc() Plane's Most Interesting Zone",
col=colors_for_fig7,
pch=20,
cex=sizes_for_fig7)
show_landmarks()
Figure 7
I’ve exaggerated the variation in the black line’s thickness to help us see where the most even scale lies in Figure 7. The line seems to be thickest at about an x value of 1.6 and a y value of 3.4. Of course, we don’t have to eyeball this, since we can just check the evenness values directly:
which_most_even <- which.min(evenness_values)
projected_scales[, which_most_even]
#> [1] 0.000000 1.643395 3.356605 5.000000 7.000000 8.643395 10.356605
We might have guessed that the maximum evenness was at the round numbers of \(1 \frac{2}{3}\) and \(3 \frac{1}{3}\), but the real answer is is somewhat more surprising:
naive_guess <- scale_from_genus(c(1+(2/3), 3+(1/3)))
actual_optimum <- scale_from_genus(c(23/14, 47/14))
evenness(naive_guess)
#> [1] 0.2519763
evenness(actual_optimum)
#> [1] 0.2474358
Since evenness()
returns lower values for more even
scales, the actual_optimum
scale with scale degree 2 at
\(\frac{23}{14}\) semitones (about
1.6428) is in fact the most even scale on the black line in Figure 7.
(The numbers \(\frac{23}{14}\) and
\(\frac{47}{14}\) may feel a bit less
like they come out of nowhere when we notice that their difference is
\(\frac{12}{7}\), meaning that the
scale has a step from \(\hat{2}\) to
\(\hat{3}\) which exactly matches the
equiheptatonic scale’s step size.) Let’s plot this scale as landmark
7:
zoomed_tetra_plot(points_for_fig7,
"Most Even Scale at the Point Labeled #7",
col=colors_for_fig7,
pch=20,
cex=sizes_for_fig7)
show_landmarks()
points(23/14, 47/14, pch=19, cex=2.5, col="white")
points(23/14, 47/14, pch="7", font=2)
Figure 8
I was surprised by this when I wrote my first draft of this vignette,
especially since the naive_guess
scale is not only a
rounder number but also a more regular scale. It’s at the point where 3
lines intersect just down and right of landmark 7: it’s thus
one-dimensional and, as it turns out, well-formed:
I was surprised because I’d fallen into a simple trap: assuming that
all apparently desirable qualities ought to coincide in one scale. This
was an easy assumption to make because so much of Western music theory
is devoted to the notion that basic musical structures (like consonant
triads and the diatonic scale) are overdetermined, points where many
neat features overlap. There’s certainly truth to this, but our example
of the naive_guess
and actual_optimum
scales
is a reminder that often the situation is more like the tension between
Pythagorean and Ptolemaic “syntonic” tunings of the diatonic scale: two
desirable properties can be optimized by scales that are quite similar
but ultimately distinct from one another. Hopefully this vignette shows
how pairing direct computation with visualization strategies can help us
explore and understand such relationships.
Our approach in the previous section involved randomly sampling
scales as its first step. This is especially convenient for quick
prototyping and situations where you might want to run many computations
on concrete data points. But an obvious downside of the method, seen in
Figures 4-7, is that random sampling produces a blotchy image with gaps
between the sampled points. If your goal is primarily to create a good
image, R has better tools, particularly the function
image()
. This function requires a bit more care in the way
you enter data as the price of entry for much better pictures.
To see how it works, let’s first use image()
to
reconstruct Figure 4:
# Define a grid that covers the tc() plane evenly.
# x and y both range from 0 to 5 but with slight offsets so that we only see 3-D scales:
grid_subdivisions <- 100
x <- seq(0.001, 4.999, length.out=grid_subdivisions)
y <- seq(0, 4.98, length.out=grid_subdivisions)
# Calculate the scale structures. (Cf. the definition of `scalar_colors` for Figure 4.)
color_from_point <- function(x, y) scale_from_genus(c(x, y)) |> signvector() |> match_sv()
scalar_colors <- outer(x, y, Vectorize(color_from_point))
# Create visuals
color_palette <- c("aliceblue", palette.colors(26, palette="Polychrome 36"))
oldpar <- par(bg="aliceblue")
image(x, y, z=scalar_colors, col=color_palette,
xlab="Height of Scale Degree 2", ylab="Height of Scale Degree 3")
mtext(side=3, "26 Scalar Colors in the tc() Plane", font=2, line=1)
show_landmarks()
Figure 9
Each region in this figure is now a continuous patch of color. The
boundaries between colors ought to be straight lines, though in Figure 9
the diagonal lines are pixelated. That’s because image()
creates the plot by dividing the plane into a grid of squares to be
colored. We could get a smoother-looking plot by dividing the image more
finely: you could try increasing grid_subdivisions
in the
above code block to a number greater than 100, though the larger it
gets, the more time is required to create the image.
The image()
function is especially useful for
visualizing features that vary continuously. One such value is scalar
evenness()
, as we explored in Figures 6-8. Let’s use
image()
to plot the evenness of all points on the
tc()
plane. I’ll also add a white “x” to show the location
of landmark scale #7 from Figure 8.
# Position plot and legend
oldpar <- par(no.readonly=TRUE)
layout(matrix(c(1, 2), ncol=2), widths=c(5, 2))
# Plot the evenness values
measure_evenness <- function(x, y) scale_from_genus(c(x, y)) |> evenness()
evenness_values <- outer(x, y, Vectorize(measure_evenness))
fig10_palette <- hcl.colors(100, palette="Green-Brown")
par(bg="aliceblue")
image(x, y, z=evenness_values, col=fig10_palette,
xlab="Height of Scale Degree 2", ylab="Height of Scale Degree 3")
points(23/14, 47/14, pch=4, col="white", cex=1.5)
mtext(side=3, "Evenness of Scales in the tc() Plane", font=2, line=1)
# Make the legend
color_legend <- as.raster(matrix(rev(fig10_palette), ncol=1))
ymin <- min(evenness_values[!is.na(evenness_values)])
ymax <- max(evenness_values[!is.na(evenness_values)])
plot(c(0, 1), c(ymin, ymax), type="n", axes=FALSE, xlab="", ylab="Evenness")
rasterImage(color_legend, 0, ymin, 1, ymax)
axis(2, at = round(seq(ymin, ymax, length.out=5), 2))
Figure 10
The most even scales (dark green) are centered around the “x” that
represents landmark #7 (the actual_optimum
from before).
This tells us that the actual_optimum
is not only the most
even scale on the flat of double_harmonic
but also the most
even scale on the entire tc()
plane. Otherwise, Figure 10
doesn’t tell us much that seems interesting: evenness decreases as you
move away from the actual_optimum
in any direction. (It
decreases fastest if you move along the line of inversional symmetry and
slowest if you move perpendicular to that line.)
A much more interesting picture emerges if we plot the so-called
“brightness ratio” value for the scales on the tc()
plane
(values that can be calculated with musicMCT’s ratio()
function).
oldpar <- par(no.readonly=TRUE)
layout(matrix(c(1, 2), ncol=2), widths=c(5, 2))
# Plot the brightness ratio values
measure_ratio <- function(x, y) scale_from_genus(c(x, y)) |> ratio()
ratio_values <- outer(x, y, Vectorize(measure_ratio))
fig11_palette <- hcl.colors(100, palette="Green-Brown")
par(bg="aliceblue")
image(x, y, z=ratio_values, col=fig11_palette,
xlab="Height of Scale Degree 2", ylab="Height of Scale Degree 3")
mtext(side=3, '"Brightness Ratio" Values in the tc() Plane', font=2, line=1)
# Make the legend
color_legend <- as.raster(matrix(rev(fig11_palette), ncol=1))
ymin <- min(ratio_values[!is.na(ratio_values)])
ymax <- max(ratio_values[!is.na(ratio_values)])
plot(c(0, 1), c(ymin, ymax), type="n", axes=FALSE, xlab="", ylab="Brightness Ratio")
rasterImage(color_legend, 0, ymin, 1, ymax)
axis(2, at = round(seq(ymin, ymax, length.out=5), 2))
Figure 11
This is our clearest picture yet of the space’s inversional symmetry.
We can see a few spots in the tc()
plane where the color
become a deep brown, representing high ratio()
values. As I
argue in “Modal Color Theory,” low “brightness ratio” values seem to be
a desirable property, particularly when the value drops below 1 so that
a scale’s delta()
value is smaller than eps()
.
There are indeed some parts of the plane where ratio()
dips
below 1. While the legend shows that this corresponds to dark greek
colors, the color scheme doesn’t make the distinction between
> 1
and < 1
really easy to spot. We
could play around with other color schemes, but we could instead take
advantage of another function that works nicely in tandem with
image()
: contour()
can plot contour lines that
show us where specific values in the plot occur:
oldpar <- par(no.readonly=TRUE)
layout(matrix(c(1, 2), ncol=2), widths=c(5, 2))
# Plot the brightness ratio values
measure_ratio <- function(x, y) scale_from_genus(c(x, y)) |> ratio()
ratio_values <- outer(x, y, Vectorize(measure_ratio))
fig11_palette <- hcl.colors(100, palette="Green-Brown")
par(bg="aliceblue")
image(x, y, z=ratio_values, col=fig11_palette,
xlab="Height of Scale Degree 2", ylab="Height of Scale Degree 3")
mtext(side=3, 'Contour Plot of "Brightness Ratio" in the tc() Plane', font=2, line=1)
# Add the contour plot
contour(x, y, z=ratio_values, add=TRUE)
# Make the legend
color_legend <- as.raster(matrix(rev(fig11_palette), ncol=1))
ymin <- min(ratio_values[!is.na(ratio_values)])
ymax <- max(ratio_values[!is.na(ratio_values)])
plot(c(0, 1), c(ymin, ymax), type="n", axes=FALSE, xlab="", ylab="Brightness Ratio")
rasterImage(color_legend, 0, ymin, 1, ymax)
axis(2, at = round(seq(ymin, ymax, length.out=5), 2))
Figure 12
From this, we certainly have a much clearer picture of which scales
have a ratio()
value below 1. But, as before, there’s a lot
of detail at the center of the plot which is hard to make out in Figure
12, so let’s zoom in to the central square like we did in Figure 7.
oldpar <- par(no.readonly=TRUE)
layout(matrix(c(1, 2), ncol=2), widths=c(5, 2))
grid_subdivisions <- 100
u <- seq(1.001, 1.999, length.out=grid_subdivisions)
v <- seq(3, 3.98, length.out=grid_subdivisions)
ratio_values2 <- outer(u, v, Vectorize(measure_ratio))
fig11_palette <- hcl.colors(100, palette="Green-Brown")
par(bg="aliceblue")
image(u, v, z=ratio_values2, col=fig11_palette,
xlab="Height of Scale Degree 2", ylab="Height of Scale Degree 3")
contour(u, v, z=ratio_values2, add=TRUE)
mtext(side=3, "\"Brightness Ratio\" at Center of tc() Plane", font=2, line=1)
# Make the legend
color_legend <- as.raster(matrix(rev(fig11_palette), ncol=1))
ymin <- min(ratio_values2[!is.na(ratio_values)])
ymax <- max(ratio_values2[!is.na(ratio_values)])
plot(c(0, 1), c(ymin, ymax), type="n", axes=FALSE, xlab="", ylab="Brightness Ratio")
rasterImage(color_legend, 0, ymin, 1, ymax)
axis(2, at = round(seq(ymin, ymax, length.out=5), 2))
Figure 13
We can see a lot more detail here! Some of the shapes in this picture
look pretty interesting, and so far I don’t know the whole story behind
them. One thing that is good to note is that our color scale has shifted
slightly: the highest ratio()
shown in Figure 13 is about
4.73, slightly more than Figure 13’s maximum value of 3.89. That’s
because we’ve zoomed further in toward the highly concentrated brown
patches, like the one centered at (1.5, 3.5). The brown patches’ centers
are all points that represent scales with one degree of freedom. For
instance, the point (1.5, 3.5) is an intersection of 4 distinct lines as
you can check by going back to Figure 9. These 1-D scales actually tend
to have very low ratio()
values. For instance:
gives the result of 0 because that scale is yet another one in the
tc()
plane with the property of well-formedness. Scales
that get close to well-formed scales (or other scales of high
regularity) tend to experience steep increases in their
ratio()
value. This means that visualizations of
ratio()
like Figure 13 can often be used to spot
high-regularity scales by the patches of high “brightness ratio” that
tend to surround them. The very regular scales like
scale_from_genus(c(1.5, 3.5))
don’t themselves show up on
Figure 13 because, like Figure 4, Figure 13 only plots scales with three
degrees of freedom. Even if the more regular scales were plotted
directly on the figure, they would be largely drowned out by the more
numerous 3-D scales since they represent only individual points.
Visualizing a space can have powerful effects, making the space feel
more real and more intuitively graspable. This is certainly a good
reason to do it, and the intuitions that you can build about MCT spaces
from exercises like this vignette are legitimately useful. But while
doing so we also need to remind ourselves of the limitations of the
method chosen. In the case of the tc()
plane, a significant
danger lies in how drastically it oversimplifies the heptachord
arrangement: the tc()
plane itself is a tiny sliver of the
space. We shouldn’t let its simplicity mislead us. For instance, looking
back at Figure 9, we see that the well-formed major scale (landmark #1)
is surrounded by 6 distinct colorful regions (i.e. six 3-D structures)
and 6 line segments (i.e. six 2-D structures). Those six neighboring
colors are a minuscule fraction of all the colors that neighbor the
major scale’s hue. It has 22,506 neighbors (plus one for the “white”
equiheptatonic). Most of these are 4-, 5-, or 6-D and could never have
shown up on the tc()
plane. But 332 of its neighbors are
2-D and 2852 of them are 3-D. Imagine 332 line segments that all touch
landmark 1, not just 6 of them! That’s a good reminder of the real
complexity of the space.
To wrap up briefly, the steps we’ve taken to build the above figures
demonstrate a few options for visualizing structures in the Modal Color
Theory spaces. In my own exploration of these spaces, I’ve found it very
helpful to be able to cook up images that give me some visual sense of
what’s going on in a region of interest. We’ve seen how one useful
approach can be to define some subspace–some slice through the many
dimensions of all possible scales–like the tc()
plane, to
reason about its relationship to the overall space, and to learn to
recognize various structures that occur within it (like the point
representing the meantone major scale or the line representing the
double harmonic scale’s flat). Sometimes, just learning to read a figure
and checking our visual intuitions against numerical values can lead to
interesting & novel observations, like in our discussion of the
naive_guess
and actual_optimum
scales around
Figures 7 and 8.
Finally, I’d be remiss not to mention that throughout this vignette all of the images we’ve seen (except for Figure 1, which I made in an external graphics program) were produced by functions that come with default installations of R. There are many packages that supplement R’s basic graphics capacities, such as the package ggplot2, which might come in handy for projects that have more complicated plotting needs.
Last updated: 3 June 2025
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.