2 Quickstart
harmonica is a Clojure library for computational group theory and representation theory.
Groups are the mathematics of symmetry. When a problem has symmetry, group theory turns brute force into elegant formulas. This page shows four examples — each solved in a few lines.
(ns harmonica-book.quickstart
(:require
[scicloj.harmonica :as hm]
[scicloj.harmonica.linalg.complex :as cx]
[tablecloth.api :as tc]
[scicloj.tableplot.v1.plotly :as plotly]
[tech.v3.datatype :as dtype]
[tech.v3.datatype.functional :as dfn]
[scicloj.kindly.v4.kind :as kind]))Counting necklaces
With 8 beads and 3 colors, there are \(3^8 = 6561\) possible colorings. But many are just rotations of each other. How many are truly distinct?
The cyclic group \(C_8\) captures rotational symmetry. Its cycle index encodes the structure of all 8 rotations, and Pólya’s theorem evaluates it — no enumeration needed.
(defn rotation-action [n]
(fn [g x] (mod (+ x g) n)))(let [G (hm/cyclic-group 8)
ci (hm/cycle-index G (rotation-action 8) (range 8))]
(hm/polya-count ci 3))834NIf flipping the necklace over also counts as “the same,” the symmetry group is the dihedral group \(D_8\) (rotations + reflections):
(defn dihedral-action [n]
(fn [[t k] x]
(case t
:r (mod (+ x k) n)
:s (mod (- k x) n))))(let [G (hm/dihedral-group 8)
ci (hm/cycle-index G (dihedral-action 8) (range 8))]
(hm/polya-count ci 3))498N834 necklaces, 498 bracelets — from the group’s structure alone. This works even for \(n = 100\) where enumeration is impossible. For the full story, see Counting Necklaces.
The DFT you already know
The Discrete Fourier Transform — behind MP3, JPEG, and spectral analysis — is the Fourier transform on the cyclic group \(\mathbb{Z}/n\mathbb{Z}\). The character table is the DFT matrix.
(def G (hm/cyclic-group 24))(def ct (hm/character-table G))Monthly temperatures (°C) over two years:
(def temperatures
[2 3 7 12 17 22 25 24 19 13 7 3
3 4 8 13 18 23 26 25 20 14 8 4])(-> (tc/dataset {:month (range 24) :temp temperatures})
(plotly/base {:=x :month :=y :temp
:=title "Monthly temperatures (°C)"
:=x-title "month" :=y-title "°C"})
(plotly/layer-line)
(plotly/layer-point {:=mark-size 5})
plotly/plot)(def f-hat (hm/fourier-transform ct (cx/complex-tensor-real temperatures)))f-hat#ComplexTensor [24]
[320.0, -1.0000000000000484+7.5957541127251424i, -137.2820323027551+7.464101615137718i, -1.0000000000000213+2.414213562373079i, 5.999999999999962-6.928203230275513i, -1.000000000000032+1.3032253728412102i, -2.000000000000009+1.9999999999999902i, -1.0000000000000342+0.7673269879789584i, 1.9999999999999751-3.108624468950438E-14i, -1.0000000000000244+0.41421356237308116i, 1.2820323027550629+0.5358983848622507i, -1.000000000000047+0.1316524975873985i, -1.9594348786357652E-14i, -1.0000000000000444-0.13165249758739717i, 1.2820323027550633-0.5358983848622372i, -1.0000000000000246-0.41421356237311135i, 1.999999999999976-4.218847493575595E-14i, -1.000000000000035-0.7673269879789588i, -2.0000000000000075-2.0000000000000098i, -1.0000000000000284-1.3032253728412089i, ... (24 total)]The Fourier magnitudes reveal which frequencies carry the signal’s energy:
(let [n 24
magnitudes (mapv (fn [k] {:k k :magnitude (cx/cabs (f-hat k))})
(range (inc (/ n 2))))]
(-> (tc/dataset magnitudes)
(plotly/base {:=x :k :=y :magnitude})
(plotly/layer-bar)
(plotly/update-data
(fn [d] (assoc d :=layout
{:title "Fourier magnitudes |f̂(k)|"
:xaxis {:title "frequency k" :dtick 1}
:yaxis {:title "|f̂(k)|"}
:width 500 :height 300})))
plotly/plot))The DC component (\(k = 0\)) is the sum of all values. The dominant non-DC component is \(k = 2\) — two cycles in 24 months, the annual cycle:
(cx/re (f-hat 0))320.0Verify \(k = 2\) dominates: its magnitude exceeds all other non-DC components.
(let [mags (mapv (fn [k] [k (cx/cabs (f-hat k))]) (range 1 (inc (/ 24 2))))]
(first (apply max-key second mags)))2Inverse transform recovers the original signal exactly:
(let [recovered (cx/re (hm/inverse-fourier-transform ct f-hat))]
(< (dfn/reduce-max (dfn/abs (dfn/- recovered temperatures))) 1e-10))trueThis generalizes: the same framework (group → character table → Fourier transform) works for any finite group. For the full story, see The DFT as Group Fourier Transform.
Symmetry you can see
Draw one curve. Apply the dihedral group \(D_6\) — rotations and reflections of a hexagon — and get a snowflake.
(defn make-rosette [n motif]
(let [matrices
(mapcat
(fn [k]
(let [a (* 2.0 Math/PI (/ (double k) (double n)))]
[[[(Math/cos a) (- (Math/sin a))]
[(Math/sin a) (Math/cos a)]]
[[(Math/cos a) (Math/sin a)]
[(Math/sin a) (- (Math/cos a))]]]))
(range n))]
(mapv (fn [[[a b] [c d]]]
(mapv (fn [[x y]] [(+ (* a x) (* b y))
(+ (* c x) (* d y))])
motif))
matrices)))(let [motif (mapv (fn [i]
(let [t (/ (double i) 30)
r (+ 0.3 (* 0.5 t))
a (* t 0.9)]
[(* r (Math/cos a)) (* r (Math/sin a))]))
(range 31))
copies (make-rosette 6 motif)
colors (cycle ["#e74c3c" "#3498db" "#2ecc71" "#f39c12" "#9b59b6" "#e67e22"
"#c0392b" "#2980b9" "#27ae60" "#d35400" "#8e44ad" "#1abc9c"])]
(kind/plotly
{:data (vec (map-indexed
(fn [i copy]
{:type "scatter" :mode "lines"
:x (mapv first copy)
:y (mapv second copy)
:line {:color (nth colors i) :width 2}
:showlegend false})
copies))
:layout {:title "D₆ rosette — 12 copies of one curve"
:xaxis {:visible false :scaleanchor "y"}
:yaxis {:visible false}
:width 400 :height 400}}))Each copy is a group element’s action on the original curve. The \(D_6\) group has 12 elements — 6 rotations and 6 reflections — producing 12 copies. For more, see Symmetry Sketchpad.
Symmetry you can hear
The Klein four-group \(V_4\) gives four ways to transform a melody: original, retrograde (backwards), inversion (upside down), and retrograde inversion (both). Composers from Bach to Schoenberg have used these symmetries as compositional tools.
(def V4 (hm/product-group (hm/cyclic-group 2) (hm/cyclic-group 2)))The subject of Bach’s Art of Fugue — a theme Bach designed to work under all four \(V_4\) transformations. Pairs of [pitch duration] (MIDI note numbers, duration in seconds):
(def motif [[62 0.8] [69 0.8] [65 0.8] [62 0.5] [61 0.5] [62 0.5] [64 0.5] [65 0.8]])(defn apply-v4 [pivot [r i] melody]
(let [m (if (= i 1) (mapv (fn [[p d]] [(- (* 2 pivot) p) d]) melody) melody)]
(if (= r 1) (vec (reverse m)) m)))The four forms:
(let [pivot (ffirst motif)]
{:original (mapv first motif)
:retrograde (mapv first (apply-v4 pivot [1 0] motif))
:inversion (mapv first (apply-v4 pivot [0 1] motif))
:retrograde-inv (mapv first (apply-v4 pivot [1 1] motif))}){:original [62 69 65 62 61 62 64 65],
:retrograde [65 64 62 61 62 65 69 62],
:inversion [62 55 59 62 63 62 60 59],
:retrograde-inv [59 60 62 63 62 59 55 62]}Synthesize and play them:
(def sample-rate 44100.0)(defn play [melody]
(let [amp 2500.0
offsets (reductions + 0 (map (fn [[_ d]] (long (* d sample-rate))) melody))
total (last offsets)]
(kind/audio
{:sample-rate sample-rate
:samples
(dtype/make-reader
:float32 total
(let [[note-idx note-start]
(loop [i 0]
(if (< idx (nth offsets (inc i)))
[i (nth offsets i)]
(recur (inc i))))
[pitch dur] (melody note-idx)
n-note (long (* dur sample-rate))
t (- idx note-start)
attack (long (* 0.015 sample-rate))
sounding (long (* 0.85 n-note))
release (long (* 0.06 sample-rate))
freq (* 440.0 (Math/pow 2.0 (/ (- (double pitch) 69.0) 12.0)))
phase (/ (double t) sample-rate)]
(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))))
wave (+ (* 0.65 (Math/sin (* 2.0 Math/PI freq phase)))
(* 0.25 (Math/sin (* 2.0 Math/PI 2.0 freq phase)))
(* 0.10 (Math/sin (* 2.0 Math/PI 3.0 freq phase))))]
(float (* amp env wave))))))})))Traverse the whole group — each element transforms the motif differently:
(play motif)(play (apply-v4 (ffirst motif) [1 0] motif))(play (apply-v4 (ffirst motif) [0 1] motif))(play (apply-v4 (ffirst motif) [1 1] motif))For twelve-tone rows, transposition, and more musical group theory, see Hearing Symmetry.
The library at a glance
harmonica provides:
| Concept | Groups |
|---|---|
| Cyclic groups Z/nZ | Clock arithmetic, DFT |
| Dihedral groups Dₙ | Polygon symmetries, necklaces |
| Symmetric groups Sₙ | Permutations, card shuffling |
| Product groups G₁ × G₂ | 2D DFT, Klein four-group |
| Tool | Purpose |
|---|---|
| Character tables | Fourier-analytic backbone |
| Fourier transform | Decompose functions on groups |
| Group actions | Orbits, Burnside counting, Pólya |
| Representation matrices | Explicit matrix irreps via Young tableaux |
Where to start
Groups and Structure — what groups are and the group families in the library
The DFT as Group Fourier Transform — the connection between the DFT and cyclic groups
Random Walks and Convolution — convergence to uniform via Fourier eigenvalues
Symmetry Sketchpad — draw rosette patterns with dihedral groups
Counting Necklaces — Burnside’s lemma and Pólya enumeration
Chord Geometry — music theory as group action
Hearing Symmetry — the Klein four-group in melodic transformations
Random Transpositions — the cutoff phenomenon in card shuffling
For a look at the internal data structures and algorithms, see Under the Hood.