9 Chord Geometry — Music Theory as Group Action
Western music’s 12 pitch classes form the cyclic group \(\mathbb{Z}/12\mathbb{Z}\). Chords are subsets. Two chords related by transposition — shifting all notes by the same interval — are the “same type”: C major and D major are both “major.” Chord types are orbits under the group action.
This notebook uses group actions to classify chords, connecting abstract algebra to something every musician knows intuitively.
(ns harmonica-book.chord-geometry
(:require
[scicloj.harmonica :as hm]
[scicloj.lalinea.tensor :as t]
[scicloj.lalinea.elementwise :as el]
[harmonica-book.book-helpers :refer [allclose?]]
[tablecloth.api :as tc]
[scicloj.tableplot.v1.plotly :as plotly]
[scicloj.kindly.v4.kind :as kind]))Audio Helpers
To hear what group actions do to chords, we need a simple synthesizer. Each note is a sum of harmonics with an exponential decay envelope.
(def sample-rate 44100.0)(defn midi->freq
"MIDI note number to frequency. A4 (69) = 440 Hz."
[midi]
(* 440.0 (Math/pow 2.0 (/ (- midi 69.0) 12.0))))(defn chord->samples
"Render a chord (collection of MIDI notes) as audio samples."
[midi-notes duration]
(let [n-samples (long (* duration sample-rate))
amp (/ 2500.0 (count midi-notes))
attack (long (* 0.02 sample-rate))
release (long (* 0.1 sample-rate))]
(t/make-reader
:float32
n-samples
(let [env (cond
(< idx attack) (/ (double idx) attack)
(> idx (- n-samples release))
(/ (double (- n-samples idx)) release)
:else (Math/exp (* -1.0 (/ (double (- idx attack)) n-samples))))
phase (/ (double idx) sample-rate)
wave (reduce + (map (fn [m]
(let [f (midi->freq m)]
(+ (* 0.65 (Math/sin (* 2.0 Math/PI f phase)))
(* 0.25 (Math/sin (* 2.0 Math/PI 2.0 f phase)))
(* 0.10 (Math/sin (* 2.0 Math/PI 3.0 f phase))))))
midi-notes))]
(float (* amp env wave))))))(defn chord-sequence->samples
"Render a sequence of chords as audio. Each chord is a collection of MIDI notes."
[chords chord-dur]
(let [n-chord (long (* chord-dur sample-rate))
n-total (* (count chords) n-chord)
amp-per-note 2500.0
attack (long (* 0.015 sample-rate))
sounding (long (* 0.85 n-chord))
release (long (* 0.06 sample-rate))]
(t/make-reader
:float32
n-total
(let [chord-idx (quot idx n-chord)
t (rem idx n-chord)
midi-notes (nth chords chord-idx)
n-notes (count midi-notes)
amp (/ amp-per-note n-notes)]
(if (>= t sounding)
(float 0.0)
(let [env (cond
(< t attack) (/ (double t) attack)
(> t (- sounding release))
(* (Math/exp (* -1.5 (/ (double (- t attack)) sounding)))
(/ (double (- sounding t)) release))
:else (Math/exp (* -1.5 (/ (double (- t attack)) sounding))))
phase (/ (double t) sample-rate)
wave (reduce + (map (fn [m]
(let [f (midi->freq m)]
(+ (* 0.65 (Math/sin (* 2.0 Math/PI f phase)))
(* 0.25 (Math/sin (* 2.0 Math/PI 2.0 f phase)))
(* 0.10 (Math/sin (* 2.0 Math/PI 3.0 f phase))))))
midi-notes))]
(float (* amp env wave))))))))(defn play-chord
"Play a chord given as pitch-class numbers (0-11). Octave is C4 (MIDI 60)."
[pcs]
(let [midi (mapv #(+ 60 %) (sort pcs))]
(kind/audio {:samples (chord->samples midi 1.5)
:sample-rate sample-rate})))(defn play-chords
"Play a sequence of chords. Each chord is a set of pitch-class numbers."
[chord-seq]
(let [midi-chords (mapv (fn [pcs] (mapv #(+ 60 %) (sort pcs))) chord-seq)]
(kind/audio {:samples (chord-sequence->samples midi-chords 0.6)
:sample-rate sample-rate})))Pitch Classes on the Clock
The 12 pitch classes {C, C#, D, …, B} form a circle, like hours on a clock. We label them 0 through 11:
(def pitch-names
["C" "C#" "D" "D#" "E" "F" "F#" "G" "G#" "A" "A#" "B"])A chord is a subset of this circle. For example, C major = {C, E, G} = {0, 4, 7}. On the clock face, it’s a triangle inscribed in the circle.
(defn chord-plot
"Draw a chord as a polygon on the pitch class circle."
[pcs title]
(let [n 12
angles (mapv (fn [i] (- (* 2 Math/PI (/ i (double n))) (/ Math/PI 2))) (range n))
xs (mapv #(Math/cos %) angles)
ys (mapv #(Math/sin %) angles)
pcs-sorted (vec (sort pcs))
chord-xs (mapv (fn [i] (xs i)) pcs-sorted)
chord-ys (mapv (fn [i] (ys i)) pcs-sorted)
colors (mapv (fn [i] (if ((set pcs) i) "#e74c3c" "#bdc3c7")) (range n))
sizes (mapv (fn [i] (if ((set pcs) i) 14 8)) (range n))]
(kind/plotly
{:data [{:type "scatter" :mode "markers+text"
:x (vec xs) :y (vec ys)
:text (vec pitch-names) :textposition "top center"
:marker {:size (vec sizes) :color (vec colors)}
:showlegend false}
{:type "scatter" :mode "lines"
:x (conj chord-xs (first chord-xs))
:y (conj chord-ys (first chord-ys))
:line {:color "#e74c3c" :width 2}
:fill "toself" :fillcolor "rgba(231,76,60,0.15)"
:showlegend false}]
:layout {:title title
:xaxis {:visible false :scaleanchor "y"}
:yaxis {:visible false}
:width 350 :height 350
:margin {:t 40 :b 10 :l 10 :r 10}}})))(chord-plot #{0 4 7} "C major on the pitch class circle")(play-chord #{0 4 7})Transposition as Group Action
Transposition by \(k\) semitones shifts every note: \(T_k(x) = x + k \pmod{12}\). The 12 transpositions form the cyclic group \(C_{12}\).
C major = {0, 4, 7}. Transposing by 2 gives D major = {2, 6, 9}. All major chords are in the same orbit under \(C_{12}\).
(let [c-major [0 4 7]
orbit (mapv (fn [k]
(let [transposed (sort (mapv #(mod (+ % k) 12) c-major))]
{:transposition k
:notes (str (mapv pitch-names transposed))}))
(range 12))]
(kind/table
{:column-names ["Transposition" "Chord"]
:row-vectors (mapv (fn [{:keys [transposition notes]}]
[transposition notes])
orbit)}))| Transposition | Chord |
|---|---|
| 0 | ["C" "E" "G"] |
| 1 | ["C#" "F" "G#"] |
| 2 | ["D" "F#" "A"] |
| 3 | ["D#" "G" "A#"] |
| 4 | ["E" "G#" "B"] |
| 5 | ["C" "F" "A"] |
| 6 | ["C#" "F#" "A#"] |
| 7 | ["D" "G" "B"] |
| 8 | ["C" "D#" "G#"] |
| 9 | ["C#" "E" "A"] |
| 10 | ["D" "F" "A#"] |
| 11 | ["D#" "F#" "B"] |
Hear the first four transpositions — same shape, different pitch:
(play-chords [[0 4 7] [1 5 8] [2 6 9] [3 7 10]])All 12 results are different — major chords form a single orbit of size 12.
Classifying Trichords
A trichord is any 3-note subset of \(\mathbb{Z}/12\mathbb{Z}\). There are \(\binom{12}{3} = 220\) trichords in total. How many distinct types are there up to transposition?
(let [G (hm/cyclic-group 12)
act (fn [g x] (mod (+ x g) 12))
{:keys [domain] act-sub :act} (hm/subset-action act (range 12) 3)
orbs (hm/orbits G act-sub domain)]
(count orbs))19There are exactly 19 trichord types under transposition. These are known as set classes in music theory, cataloged by Allen Forte.
Let’s list them with their interval content:
(defn interval-vector
"Compute the interval vector of a pitch class set.
Counts the number of each interval class (1 through 6)."
[pcs]
(let [pcs-vec (vec (sort pcs))
n (count pcs-vec)
intervals (for [i (range n)
j (range (inc i) n)]
(let [diff (mod (- (pcs-vec j) (pcs-vec i)) 12)]
(min diff (- 12 diff))))]
(mapv (fn [ic] (count (filter #{ic} intervals)))
(range 1 7))))(let [G (hm/cyclic-group 12)
act (fn [g x] (mod (+ x g) 12))
{:keys [domain] act-sub :act} (hm/subset-action act (range 12) 3)
orbs (hm/orbits G act-sub domain)
rows (sort-by first
(mapv (fn [orb]
(let [rep (first (sort orb))
iv (interval-vector rep)]
[rep iv (count orb)]))
orbs))]
(kind/table
{:column-names ["Representative" "Interval vector" "Orbit size"]
:row-vectors (mapv (fn [[rep iv size]]
[(str rep) (str iv) size])
rows)}))| Representative | Interval vector | Orbit size |
|---|---|---|
| [0 1 2] | [2 1 0 0 0 0] | 12 |
| [0 1 3] | [1 1 1 0 0 0] | 12 |
| [0 1 4] | [1 0 1 1 0 0] | 12 |
| [0 1 5] | [1 0 0 1 1 0] | 12 |
| [0 1 6] | [1 0 0 0 1 1] | 12 |
| [0 1 7] | [1 0 0 0 1 1] | 12 |
| [0 1 8] | [1 0 0 1 1 0] | 12 |
| [0 1 9] | [1 0 1 1 0 0] | 12 |
| [0 1 10] | [1 1 1 0 0 0] | 12 |
| [0 2 4] | [0 2 0 1 0 0] | 12 |
| [0 2 5] | [0 1 1 0 1 0] | 12 |
| [0 2 6] | [0 1 0 1 0 1] | 12 |
| [0 2 7] | [0 1 0 0 2 0] | 12 |
| [0 2 8] | [0 1 0 1 0 1] | 12 |
| [0 2 9] | [0 1 1 0 1 0] | 12 |
| [0 3 6] | [0 0 2 0 0 1] | 12 |
| [0 3 7] | [0 0 1 1 1 0] | 12 |
| [0 3 8] | [0 0 1 1 1 0] | 12 |
| [0 4 8] | [0 0 0 3 0 0] | 4 |
Every row is a distinct chord type. Some orbits have size 12 (the chord is not symmetric under any transposition), others have smaller orbits when the chord has internal symmetry — like the augmented triad {0, 4, 8} with orbit size 4, or the diminished triad.
Adding Inversion: The Dihedral Group
Inversion maps pitch class \(x\) to \(-x \pmod{12}\) — it flips intervals upside down. Combined with transposition, the symmetry group becomes the dihedral group \(D_{12}\) (order 24).
Under \(D_{12}\), some trichord types that were distinct under \(C_{12}\) get merged. For example, a chord and its inversion become the same type.
(let [G (hm/dihedral-group 12)
act (fn [[t k] x]
(case t
:r (mod (+ x k) 12)
:s (mod (- k x) 12)))
{:keys [domain] act-sub :act} (hm/subset-action act (range 12) 3)
orbs (hm/orbits G act-sub domain)]
(count orbs))12Only 12 types remain under \(D_{12}\), down from 19. The 7 pairs that merged are trichords related by inversion.
Hear the difference: C major {0, 4, 7} and its inversion {0, 8, 5} = {0, 5, 8} (= F minor). Inversion flips a major third + minor third into minor third + major third:
(play-chords [[0 4 7] [0 5 8]])Let’s see which types merged:
(let [G-c (hm/cyclic-group 12)
G-d (hm/dihedral-group 12)
act-c (fn [g x] (mod (+ x g) 12))
act-d (fn [[t k] x]
(case t
:r (mod (+ x k) 12)
:s (mod (- k x) 12)))
{domain-3 :domain act-c-sub :act} (hm/subset-action act-c (range 12) 3)
{_ :domain act-d-sub :act} (hm/subset-action act-d (range 12) 3)
orbs-c (hm/orbits G-c act-c-sub domain-3)
orbs-d (hm/orbits G-d act-d-sub domain-3)
;; For each C12 orbit, find which D12 orbit contains its representative
c-reps (mapv (fn [orb] (first (sort orb))) orbs-c)
d-orbit-of (fn [rep]
(first (filter #(contains? % rep) orbs-d)))
;; Group C12-reps by their D12 orbit
merged-groups (group-by d-orbit-of c-reps)
merged-rows (sort-by (comp str first first)
(mapv (fn [[_ reps]]
[(mapv str (sort reps))
(count reps)])
merged-groups))]
(kind/table
{:column-names ["C\u2081\u2082 types merged" "Count"]
:row-vectors (mapv (fn [[reps cnt]]
[(str reps) cnt])
merged-rows)}))| C₁₂ types merged | Count |
|---|---|
| ["[0 1 2]"] | 1 |
| ["[0 1 3]" "[0 1 10]"] | 2 |
| ["[0 1 4]" "[0 1 9]"] | 2 |
| ["[0 1 5]" "[0 1 8]"] | 2 |
| ["[0 1 6]" "[0 1 7]"] | 2 |
| ["[0 2 4]"] | 1 |
| ["[0 2 5]" "[0 2 9]"] | 2 |
| ["[0 2 6]" "[0 2 8]"] | 2 |
| ["[0 2 7]"] | 1 |
| ["[0 3 6]"] | 1 |
| ["[0 3 7]" "[0 3 8]"] | 2 |
| ["[0 4 8]"] | 1 |
The rows with count 2 are pairs of inversionally related chord types that become one type under \(D_{12}\). Count 1 means the chord type is its own inversion (palindromic interval structure).
Interval Vectors and the Z-Relation
The interval vector counts how many of each interval class a chord contains. Most chord types have unique interval vectors, but occasionally two different types share the same one — the Z-relation.
For trichords under \(D_{12}\), no Z-relation occurs. Let’s verify:
(let [G (hm/dihedral-group 12)
act (fn [[t k] x]
(case t
:r (mod (+ x k) 12)
:s (mod (- k x) 12)))
{:keys [domain] act-sub :act} (hm/subset-action act (range 12) 3)
orbs (hm/orbits G act-sub domain)
reps (mapv #(first (sort %)) orbs)
ivs (mapv interval-vector reps)
iv-groups (group-by identity ivs)]
(every? #(= 1 (count (val %))) iv-groups))trueAll 12 trichord types (under \(D_{12}\)) have distinct interval vectors — no Z-relation among trichords.
Beyond Trichords
The same analysis applies to any chord size. Let’s count set classes for all sizes:
(let [G-c (hm/cyclic-group 12)
G-d (hm/dihedral-group 12)
act-c (fn [g x] (mod (+ x g) 12))
act-d (fn [[t k] x]
(case t
:r (mod (+ x k) 12)
:s (mod (- k x) 12)))
results (mapv (fn [k]
(let [{domain-k :domain act-c-k :act} (hm/subset-action act-c (range 12) k)
{_ :domain act-d-k :act} (hm/subset-action act-d (range 12) k)
n-trans (count (hm/orbits G-c act-c-k domain-k))
n-dihed (count (hm/orbits G-d act-d-k domain-k))]
{:k k
:subsets (count domain-k)
:under-C12 n-trans
:under-D12 n-dihed}))
(range 1 12))]
(kind/table
{:column-names ["Chord size k" "Total subsets" "Types (C\u2081\u2082)" "Types (D\u2081\u2082)"]
:row-vectors (mapv (fn [{:keys [k subsets under-C12 under-D12]}]
[k subsets under-C12 under-D12])
results)}))| Chord size k | Total subsets | Types (C₁₂) | Types (D₁₂) |
|---|---|---|---|
| 1 | 12 | 1 | 1 |
| 2 | 66 | 6 | 6 |
| 3 | 220 | 19 | 12 |
| 4 | 495 | 43 | 29 |
| 5 | 792 | 66 | 38 |
| 6 | 924 | 80 | 50 |
| 7 | 792 | 66 | 38 |
| 8 | 495 | 43 | 29 |
| 9 | 220 | 19 | 12 |
| 10 | 66 | 6 | 6 |
| 11 | 12 | 1 | 1 |
The symmetry between \(k\) and \(12-k\) is the complement relation: a \(k\)-note chord and its \((12-k)\)-note complement are in bijection.
Connecting to the Fourier Transform
The characteristic function of a chord is a function on \(\mathbb{Z}/12\mathbb{Z}\): \(f(x) = 1\) if pitch \(x\) is in the chord, \(0\) otherwise.
The Fourier transform of this function decomposes it into the irreducible representations of \(\mathbb{Z}/12\mathbb{Z}\) — the 12 “frequency” components.
(let [G (hm/cyclic-group 12)
ct (hm/character-table G)
;; C major = {0, 4, 7}
f-vals (t/complex-tensor-real (mapv (fn [x] (if (#{0 4 7} x) 1.0 0.0)) (range 12)))
f-hat (hm/fourier-transform ct f-vals)]
(kind/table
{:column-names ["Frequency k" "|f\u0302(k)|\u00b2"]
:row-vectors (mapv (fn [k]
(let [fk (f-hat k)
mag-sq (let [r (el/re fk) i (el/im fk)] (+ (* r r) (* i i)))]
[k (format "%.4f" mag-sq)]))
(range 12))}))| Frequency k | |f̂(k)|² |
|---|---|
| 0 | 9.0000 |
| 1 | 0.2679 |
| 2 | 1.0000 |
| 3 | 5.0000 |
| 4 | 3.0000 |
| 5 | 3.7321 |
| 6 | 1.0000 |
| 7 | 3.7321 |
| 8 | 3.0000 |
| 9 | 5.0000 |
| 10 | 1.0000 |
| 11 | 0.2679 |
The Fourier magnitudes are invariant under transposition — transposing by \(k\) multiplies each \(\hat{f}(j)\) by \(e^{2\pi i j k/12}\), which doesn’t change the magnitude. This is why the Fourier magnitudes detect chord type, not chord position.
Let’s verify: the major triad and its transposition by a tritone (6 semitones) have the same Fourier magnitude profile:
(let [G (hm/cyclic-group 12)
ct (hm/character-table G)
chord-a [0 4 7]
chord-b [6 10 1]
f-a (t/complex-tensor-real (mapv (fn [x] (if ((set chord-a) x) 1.0 0.0)) (range 12)))
f-b (t/complex-tensor-real (mapv (fn [x] (if ((set chord-b) x) 1.0 0.0)) (range 12)))
hat-a (hm/fourier-transform ct f-a)
hat-b (hm/fourier-transform ct f-b)]
(allclose? (el/abs hat-a) (el/abs hat-b)))trueVisualizing Chord Types
Each trichord type is a triangle on the clock face. Let’s visualize a few familiar chord types:
(chord-plot #{0 4 7} "Major triad")(play-chord #{0 4 7})(chord-plot #{0 3 7} "Minor triad")(play-chord #{0 3 7})(chord-plot #{0 4 8} "Augmented triad")(play-chord #{0 4 8})(chord-plot #{0 3 6} "Diminished triad")(play-chord #{0 3 6})The augmented triad is maximally symmetric — an equilateral triangle. Its orbit under \(C_{12}\) has only 4 elements (each transposition by 4 semitones returns the same chord). The major and minor triads are related by inversion.
Forte Numbers
Allen Forte’s catalog assigns a standard identifier to each set class under \(D_{12}\) equivalence. The Forte number \(k\text{-}m\) means the \(m\)-th set class of cardinality \(k\) in Forte’s ordering.
We can compute prime forms and assign Forte numbers from first principles using the \(D_{12}\) orbits we already have.
(defn prime-form
"Compute the prime form of a pitch class set under TnI equivalence.
The prime form is the most compact representative: transpose all
rotations and inversions to start at 0, then pick the lexicographically
smallest."
[pcs]
(let [pcs-vec (vec (sort pcs))
n 12
;; All transpositions
transpositions (for [k (range n)]
(vec (sort (map #(mod (+ % k) n) pcs-vec))))
;; All inversions followed by transpositions
inversions (for [k (range n)]
(vec (sort (map #(mod (- k %) n) pcs-vec))))
;; Normalize each to start at 0
normalize (fn [s] (let [base (first s)]
(mapv #(mod (- % base) n) s)))
candidates (map normalize (concat transpositions inversions))]
(first (sort candidates))))The 12 trichord set classes under \(D_{12}\), computed by harmonica and labeled with Forte numbers:
(let [G (hm/dihedral-group 12)
act (fn [[t k] x]
(case t
:r (mod (+ x k) 12)
:s (mod (- k x) 12)))
{:keys [domain] act-sub :act} (hm/subset-action act (range 12) 3)
orbs (hm/orbits G act-sub domain)
primes (sort (mapv (fn [orb]
(prime-form (first orb)))
orbs))
;; Forte's catalog order for trichords
forte-catalog [[0 1 2] [0 1 3] [0 1 4] [0 1 5] [0 1 6]
[0 2 4] [0 2 5] [0 2 6] [0 2 7]
[0 3 6] [0 3 7] [0 4 8]]
forte-names ["3-1" "3-2" "3-3" "3-4" "3-5"
"3-6" "3-7" "3-8" "3-9"
"3-10" "3-11" "3-12"]
musical-names ["chromatic cluster" "—" "—" "—" "Viennese trichord"
"whole-tone" "—" "—" "stack of fifths"
"diminished" "major/minor triad" "augmented triad"]]
(kind/table
{:column-names ["Forte number" "Prime form" "Interval vector" "Musical name"]
:row-vectors (mapv (fn [forte pf name]
[forte (str pf) (str (interval-vector pf)) name])
forte-names forte-catalog musical-names)}))| Forte number | Prime form | Interval vector | Musical name |
|---|---|---|---|
| 3-1 | [0 1 2] | [2 1 0 0 0 0] | chromatic cluster |
| 3-2 | [0 1 3] | [1 1 1 0 0 0] | — |
| 3-3 | [0 1 4] | [1 0 1 1 0 0] | — |
| 3-4 | [0 1 5] | [1 0 0 1 1 0] | — |
| 3-5 | [0 1 6] | [1 0 0 0 1 1] | Viennese trichord |
| 3-6 | [0 2 4] | [0 2 0 1 0 0] | whole-tone |
| 3-7 | [0 2 5] | [0 1 1 0 1 0] | — |
| 3-8 | [0 2 6] | [0 1 0 1 0 1] | — |
| 3-9 | [0 2 7] | [0 1 0 0 2 0] | stack of fifths |
| 3-10 | [0 3 6] | [0 0 2 0 0 1] | diminished |
| 3-11 | [0 3 7] | [0 0 1 1 1 0] | major/minor triad |
| 3-12 | [0 4 8] | [0 0 0 3 0 0] | augmented triad |
Verify that our computed prime forms match the standard catalog exactly:
(let [G (hm/dihedral-group 12)
act (fn [[t k] x]
(case t
:r (mod (+ x k) 12)
:s (mod (- k x) 12)))
{:keys [domain] act-sub :act} (hm/subset-action act (range 12) 3)
orbs (hm/orbits G act-sub domain)
computed (sort (mapv (fn [orb] (prime-form (first orb))) orbs))
catalog [[0 1 2] [0 1 3] [0 1 4] [0 1 5] [0 1 6]
[0 2 4] [0 2 5] [0 2 6] [0 2 7]
[0 3 6] [0 3 7] [0 4 8]]]
(= computed catalog))trueThe same approach works for tetrachords. There are 29 tetrachord set classes under \(D_{12}\):
(let [G (hm/dihedral-group 12)
act (fn [[t k] x]
(case t
:r (mod (+ x k) 12)
:s (mod (- k x) 12)))
{:keys [domain] act-sub :act} (hm/subset-action act (range 12) 4)
orbs (hm/orbits G act-sub domain)]
(count orbs))29Two of these — 4-Z15 and 4-Z29 — share the same interval vector \(\langle111111\rangle\) despite being different set classes. This is the Z-relation, the only instance among tetrachords:
(let [z15 [0 1 4 6]
z29 [0 1 3 7]]
(= (interval-vector z15) (interval-vector z29)))trueSummary
This notebook demonstrated:
- Pitch classes as \(\mathbb{Z}/12\mathbb{Z}\): the chromatic scale is a cyclic group
- Transposition as group action: \(C_{12}\) acts on pitch class subsets
- Chord types as orbits: 220 trichords \(\to\) 19 types under \(C_{12}\), 12 under \(D_{12}\)
- Interval vectors: a transposition-invariant fingerprint of a chord
- Fourier magnitudes: another invariant, connecting to representation theory
- Inversional equivalence: the dihedral group \(D_{12}\) merges chord/inversion pairs
- Forte numbers: standard catalog of set classes, computed from \(D_{12}\) orbits
- Z-relation: distinct set classes sharing the same interval vector
For another perspective on music and group theory — transforming melodies via the Klein four-group — see Hearing Symmetry.
For the general framework of Burnside counting and Pólya enumeration, see Counting Necklaces.