Skip to content

SuperInstance/holonomy-harmony

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

18 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

holonomy-harmony

Chord progression analysis via holonomy β€” detect modulations, modal interchange, and cycle violations in harmony. Proves that harmonic movement = cycle consistency on the circle of fifths.

What This Does

Holonomy-harmony treats the circle of fifths as a topological space and chord progressions as paths through it. When a progression returns to its starting tonal center, it has zero holonomy β€” the cycle is consistent. When it doesn't, the holonomy number tells you exactly how far the harmony wandered. This is constraint theory applied to music theory.

The library gives you:

  • Holonomy computation β€” net circle-of-fifths displacement, winding number, max deviation for any chord progression
  • Progression classification β€” diatonic, modal interchange, modulation, chromatic mediant, chromatic
  • Roman numeral parsing β€” full parser for I, V7/vi, bVI, iiΒ°, with secondary dominant detection
  • Tonal graph β€” directed weighted graph of pitch-class transitions with functional direction classification
  • Modulation detection β€” automatic detection of key changes, secondary dominants, and borrowed chords
  • Stability scoring β€” 0–1 scale measuring how "safe" vs "adventurous" a progression is
  • Topological analysis β€” Euler characteristic, Betti numbers, fundamental group, homology groups of harmonic spaces
  • Curvature flow β€” discrete Ricci flow that smooths tonal tension over time
  • 20 built-in progressions β€” Pachelbel Canon, Giant Steps, blues, rhythm changes, Coltrane changes, Autumn Leaves, and more

Key Idea

Each chord transition moves you clockwise (dominant) or counter-clockwise (subdominant) on the circle of fifths. Sum all movements:

Holonomy Meaning
0 Cycle closes β€” progression returned to its tonal center (diatonic)
Non-zero Cycle doesn't close β€” you modulated or borrowed from another key
Large winding number The progression spirals around the circle of fifths (Coltrane, Giant Steps)

A I-IV-V-I progression has holonomy 0 and winding number 0 β€” it's a closed walk. Giant Steps has non-zero holonomy because its major-third cycles don't close on the circle of fifths.

Install

pip install holonomy-harmony

Requires Python β‰₯ 3.10. No external dependencies (pure Python).

Quick Start

Analyze a progression

from holonomy_harmony import analyze_progression, PROGRESSIONS

# Analyze the Pachelbel Canon in D major
symbols, tonic, mode = PROGRESSIONS["pachelbel_canon"]
result = analyze_progression(symbols, key_tonic=tonic, mode=mode)

print(f"Holonomy:  {result.holonomy.holonomy}")         # -5
print(f"Winding:   {result.holonomy.winding_number}")    # 0.083
print(f"Type:      {result.holonomy.progression_type}")  # MODULATION
print(f"Stability: {result.stability_score}")             # 0.35
print(f"Chords:    {len(result.chords)}")                 # 8

Compare famous progressions

for name in ["pachelbel_canon", "blues_12_bar", "giant_steps", "axis_progression"]:
    symbols, tonic, mode = PROGRESSIONS[name]
    result = analyze_progression(symbols, key_tonic=tonic, mode=mode)
    print(f"{name:20s}  holonomy={result.holonomy.holonomy:3d}  "
          f"type={result.holonomy.progression_type.name:20s}  "
          f"stability={result.stability_score:.2f}")

# pachelbel_canon       holonomy= -5  type=MODULATION          stability=0.35
# blues_12_bar          holonomy=  0  type=DIATONIC            stability=0.88
# giant_steps           holonomy=  3  type=CHROMATIC_MEDIANT   stability=0.38
# axis_progression      holonomy=  0  type=DIATONIC            stability=0.93

Roman numeral parsing

from holonomy_harmony import parse_roman

# Simple
I = parse_roman("I", key_tonic=0, mode="major")
print(I.root, I.quality)              # 0 'maj'

# Secondary dominant: V7/vi
V7_of_vi = parse_roman("V7/vi", key_tonic=0, mode="major")
print(V7_of_vi.root)                  # 10 (A#)
print(V7_of_vi.is_secondary_dominant) # True
print(V7_of_vi.implied_key)           # (9, 'major')

# Borrowed chord: bVI in major
bVI = parse_roman("bVI", key_tonic=0, mode="major")
print(bVI.root)                       # 8 (Aβ™­)
print(bVI.is_diatonic)                # False

Holonomy computation

from holonomy_harmony import compute_holonomy, winding_number, classify_progression

roots = [0, 7, 9, 5, 0]  # C β†’ G β†’ A β†’ F β†’ C

h = compute_holonomy(roots, wrap=True)
print(h.holonomy)        # net circle-of-fifths displacement (semitones)
print(h.winding_number)  # full rotations around Co5
print(h.max_deviation)   # furthest wander from tonic
print(h.is_consistent()) # True if holonomy == 0

# Shortcuts
print(winding_number(roots))        # shortcut for winding
print(classify_progression(roots))  # ProgressionType enum

Tonal graph

from holonomy_harmony import TonalGraph

g = TonalGraph()
g.build_from_progression([0, 7, 9, 5, 0])

print(g)                          # <TonalGraph nodes=12 edges=4 total_weight=4.0>
print(g.adjacency_matrix())       # 12Γ—12 transition weight matrix
print(g.transition_probability(0, 7))  # P(next=G | current=C)
print(g.neighbors(0))             # outgoing transitions from C
print(g.weight(0, 7))             # weight of C→G transition

Topological analysis

from holonomy_harmony import TopologyAnalyzer

analyzer = TopologyAnalyzer()

# Build simplicial complex from a progression
roots = [0, 7, 9, 5, 0, 7, 2, 9, 4]
invariants = analyzer.analyze_progression(roots)

print(invariants.euler_characteristic)  # Ο‡ = V - E + F
print(invariants.betti)                 # Betti numbers (Ξ²β‚€, β₁, Ξ²β‚‚)
print(invariants.homology_0)            # Hβ‚€ β‰… Z^{components}
print(invariants.homology_1)            # H₁ β‰… Z^{holes}
print(invariants.fundamental_group)     # π₁ generators and relations
print(invariants.genus)                 # Generalized genus

Curvature flow (Ricci flow)

from holonomy_harmony import TonalGraph, CurvatureFlow, CurvatureMeasure

g = TonalGraph()
g.build_from_progression([0, 7, 9, 5, 0, 2, 7, 0])

flow = CurvatureFlow(g, dt=0.1, curvature_measure=CurvatureMeasure.HARMONIC)
state = flow.run(max_steps=100)

print(f"Converged: {state.converged}")
print(f"Steps: {state.step}")

# Most tense transitions (highest curvature)
tense = flow.most_tense_transitions(n=3)
for e in tense:
    print(f"  {e.source}β†’{e.target}: curvature={e.curvature:.3f} ({e.direction.name})")

API Reference

High-level Analysis

Function Description
analyze_progression(symbols, key_tonic, mode, wrap) Full analysis: chords β†’ graph β†’ holonomy β†’ modulations β†’ stability
detect_modulations(analysis) Extract modulation points from an analysis
score_stability(analysis) Get the stability score (0–1)

Holonomy

Function / Class Description
compute_holonomy(roots, wrap=False) Compute holonomy of a root-pitch-class progression
winding_number(roots) Net rotations around the circle of fifths
classify_progression(roots) Classify as ProgressionType enum
HolonomyResult Frozen result with .holonomy, .winding_number, .max_deviation, .progression_type, .steps, .cumulative

Roman Numerals

Function / Class Description
parse_roman(symbol, key_tonic, mode) Parse "V7/vi" β†’ Chord object
Chord Frozen dataclass: root, quality, function, key, is_diatonic, is_secondary_dominant, implied_key

Tonal Graph

Class / Method Description
TonalGraph() Directed graph of pitch-class transitions
.add_transition(from, to, weight) Add/increment a transition
.build_from_progression(roots) Populate from root sequence
.adjacency_matrix() 12Γ—12 normalised weight matrix
.transition_probability(from, to) Conditional probability P(to|from)
.neighbors(pc) Outgoing neighbors
.weight(from, to) Raw transition weight
classify_direction(from, to) Classify as DOMINANT, SUBDOMINANT, RESOLUTION, etc.

Topology

Class Description
TopologyAnalyzer(graph) Compute topological invariants
.build_complex() Build simplicial complex from tonal graph
.compute_euler() Euler characteristic Ο‡
.compute_betti() Betti numbers (Ξ²β‚€, β₁, Ξ²β‚‚)
.compute_fundamental_group() π₁ presentation (generators + relations)
.compute_homology() Hβ‚€, H₁, Hβ‚‚ as HomologyGroup objects
.analyze() All invariants at once β†’ TopologicalInvariants

Curvature Flow

Class / Method Description
CurvatureFlow(graph, dt, curvature_measure) Discrete Ricci flow
.initialize() Set up initial flow state
.step() One flow step β†’ CurvatureSnapshot
.run(max_steps, convergence_tol) Run until convergence
.most_tense_transitions(n) Top-n highest-curvature edges
.total_curvature() Sum of all edge curvatures
CurvatureMeasure.COMBINATORIAL / .HARMONIC / .RICCI Curvature definitions

Built-in Progressions

20 famous progressions in PROGRESSIONS dict:

from holonomy_harmony import PROGRESSIONS

for name, (symbols, tonic, mode) in PROGRESSIONS.items():
    print(f"{name}: {' '.join(symbols)}")

Includes: pachelbel_canon, blues_12_bar, giant_steps, chopin_em_prelude, axis_progression, andulusian_cadence, doo_wop, sensitive_female, rhythm_changes, bird_changes, coltrane_changes, autumn_leaves, creep, hey_jude, take_five, minor_ii_v_i, and more.

How It Works

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   analyzer.py   │────▢│  cycle_checker.py │────▢│ tonal_graph.py β”‚
β”‚                 β”‚     β”‚                   β”‚     β”‚                β”‚
β”‚ parse_roman()   β”‚     β”‚ compute_holonomy()β”‚     β”‚ TonalGraph     β”‚
β”‚ analyze_        β”‚     β”‚ winding_number()  β”‚     β”‚ TransitionDir  β”‚
β”‚   progression() β”‚     β”‚ classify_         β”‚     β”‚ adjacency_     β”‚
β”‚ detect_         β”‚     β”‚   progression()   β”‚     β”‚   matrix()     β”‚
β”‚   modulations() β”‚     β”‚ HolonomyResult    β”‚     β”‚ neighbors()    β”‚
β”‚ score_stability β”‚     β”‚                   β”‚     β”‚                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚                       β”‚
        β”‚               β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚               β”‚                β”‚
        β”‚       β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚       β”‚ topology.py  β”‚  β”‚    flow.py      β”‚
        β”‚       β”‚              β”‚  β”‚                 β”‚
        β”‚       β”‚ Euler char   β”‚  β”‚ CurvatureFlow   β”‚
        β”‚       β”‚ Betti nums   β”‚  β”‚ Ricci flow      β”‚
        β”‚       β”‚ π₁ group     β”‚  β”‚ CurvatureMeas   β”‚
        β”‚       β”‚ Homology H_k β”‚  β”‚ CurvatureSnap   β”‚
        β”‚       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚
        β–Ό
   Input: Roman numerals β†’ Chord objects β†’ pitch-class roots
   Process: roots β†’ circle-of-fifths steps β†’ holonomy signature
   Output: HolonomyResult + stability + modulations + topology

The Math

Holonomy on the circle of fifths

Given a chord progression with roots r₁, rβ‚‚, ..., rβ‚™, each transition rα΅’ β†’ rα΅’β‚Šβ‚ maps to a step on the circle of fifths:

step(i) = Co5_position(rα΅’β‚Šβ‚) - Co5_position(rα΅’)   (mod 12, shortest path)

The holonomy is the net displacement:

holonomy = Ξ£ step(i)   (converted back to semitones via Γ—7 mod 12)
  • holonomy = 0: the progression closed in the same key
  • holonomy β‰  0: you ended up in a different key than you started

The winding number counts full rotations:

winding = Ξ£ step(i) / 12

Stability score

score = (diatonic_count / total_chords)
      Γ— penalty(holonomy β‰  0)
      Γ— penalty(max_deviation > threshold)
      Γ— penalty(modulations)
      Γ— penalty(modal_interchanges)

Clamped to [0, 1]. A fully diatonic I-IV-V-I scores ~1.0. Giant Steps scores ~0.38.

Simplicial complex

The harmonic space is modeled as a simplicial complex:

  • 0-simplices: pitch classes that appear in the progression
  • 1-simplices: directed edges between consecutive roots
  • 2-simplices: triangles β€” triples of pitch classes forming closed 3-cycles

From this complex we compute Euler characteristic (Ο‡ = V βˆ’ E + F), Betti numbers (Ξ²β‚€ = components, β₁ = independent loops, Ξ²β‚‚ = enclosed cavities), and the fundamental group π₁.

Discrete Ricci flow

Edge weights evolve according to:

dw(e)/dt = βˆ’ΞΊ(e) Β· w(e)

Where ΞΊ(e) is the curvature on edge e. Three curvature measures are supported:

  • Combinatorial: ΞΊ = 1/d_s + 1/d_t βˆ’ w
  • Harmonic: combinatorial + tonal direction bonus (dominant = high curvature, resolution = negative)
  • Ollivier-Ricci: ΞΊ = 1 βˆ’ W₁(ΞΌβ‚“, ΞΌα΅§) where W₁ is the Wasserstein-1 transport distance

Testing

pip install pytest
pytest tests/ -v

183 tests across 5 test files covering tonal graph, cycle checker, analyzer, and topology.

License

MIT

About

🎼 Chord progression analysis via holonomy β€” detect modulations, modal interchange, and cycle violations in harmony

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors