Real-time counterpoint engine proving music IS constraint satisfaction
Project description
counterpoint-engine
๐ต Species counterpoint as constraint satisfaction โ every rule returns SAT/UNSAT, voices form a Laman graph.
What It Does
Generates multi-voice counterpoint against a cantus firmus using backtracking search over musical constraints, then outputs the result as Tensor-MIDI events. Each contrapuntal rule is a predicate returning "SAT" or "UNSAT"; each voice is a vertex in a Laman graph; every constraint is an edge.
Why It Exists
Species counterpoint has been taught as a set of prohibitions for centuries. This library treats those prohibitions as constraint predicates and proves that the constraint graph on N voices is a Laman graph (2Nโ3 edges, minimally rigid). That guarantees no voice is redundant and every rule is load-bearing. If you remove any edge, the structure gains a degree of freedom โ a voice can drift unconstrained.
The math: a set of N points in the plane is rigid iff the bar-and-joint framework on those points is Laman. Counterpoint voices are the points; interval constraints are the bars.
Quick Start
pip install -e .
from counterpoint_engine.generator import CounterpointGenerator, Species, Scale, VoiceRange
# Define a cantus firmus (C major, 8 notes)
cantus = [60, 62, 64, 65, 67, 69, 71, 72] # C D E F G A B C
gen = CounterpointGenerator(
cantus_firmus=cantus,
species=Species.FIRST,
scale=Scale(tonic=0, mode="major"),
voice_range=VoiceRange(min_pitch=48, max_pitch=67),
)
counterpoint = gen.generate()
print(counterpoint)
# โ [48, 53, 52, 50, 48, 48, 50, 48]
# Multi-voice โ Laman graph guarantees independence
voices = gen.generate_n_voices(n_voices=4)
# voices[0] = cantus firmus, voices[1..3] = generated
# Tensor-MIDI output
from counterpoint_engine.tensor_output import voices_to_tensor_events
tensor_events, midi_events = voices_to_tensor_events(voices)
print(tensor_events[0].to_bytes()) # b'\x3c\x00\x00\x0c'
API Overview
Rules (counterpoint_engine.rules)
Every rule returns the string "SAT" or "UNSAT".
from counterpoint_engine.rules import (
no_parallel_fifths, no_parallel_octaves, proper_resolution,
max_leap_seventh, consonant_interval, voice_independence, SAT, UNSAT
)
voice_a = [60, 62, 64, 65]
voice_b = [67, 69, 67, 69]
beats = [0, 1, 2, 3]
assert no_parallel_fifths(voice_a, voice_b, beats) == SAT
assert consonant_interval(voice_a, voice_b, 0) == SAT
| Function | Signature | What it checks |
|---|---|---|
no_parallel_fifths |
(voice_a, voice_b, beats) โ str |
No consecutive perfect fifths in similar motion |
no_parallel_octaves |
(voice_a, voice_b, beats) โ str |
No consecutive perfect octaves in similar motion |
proper_resolution |
(voice, beat, key_tonic, key_leading) โ str |
Leading tone resolves to tonic |
max_leap_seventh |
(voice, beat, max_leap) โ str |
Melodic leap โค minor seventh (10 semitones) |
consonant_interval |
(voice_a, voice_b, beat, allowed) โ str |
Interval at beat is a consonance |
voice_independence |
(laman_check: bool) โ str |
Constraint graph is Laman rigid |
Laman Graphs (counterpoint_engine.laman_counterpoint)
from counterpoint_engine.laman_counterpoint import (
CounterpointGraph, henneberg_construct, verify_rigidity
)
graph = CounterpointGraph(n_voices=4)
print(graph.edges) # [(0,1), (0,2), (1,2), ...]
print(graph.verify_rigidity()) # True
print(graph.edge_count()) # 5 (= 2*4 - 3)
print(graph.is_minimally_rigid()) # True
edges = henneberg_construct(4, seed=42)
assert verify_rigidity(4, edges)
| Class/Function | Description |
|---|---|
CounterpointGraph |
Laman graph with add_constraint(), verify_rigidity(), is_minimally_rigid() |
henneberg_construct(n, seed) |
Build a Laman graph via Henneberg type-I construction |
verify_rigidity(n_voices, edges) |
Check Laman conditions (2Nโ3 edges + subset condition) |
Generator (counterpoint_engine.generator)
from counterpoint_engine.generator import CounterpointGenerator, Species, Scale, VoiceRange
gen = CounterpointGenerator(
cantus_firmus=[60, 62, 64, 65, 67, 69, 71, 72],
species=Species.FIRST,
scale=Scale(tonic=0, mode="major"),
voice_range=VoiceRange(min_pitch=48, max_pitch=72),
)
# Single voice
counterpoint = gen.generate()
# Multi-voice
voices = gen.generate_n_voices(n_voices=4)
| Class/Enum | Key attributes |
|---|---|
Species |
FIRST, SECOND, THIRD, FOURTH, FIFTH (IntEnum 1โ5) |
VoiceRange |
min_pitch, max_pitch, candidates(scale, prev_pitch) |
Scale |
tonic, mode ("major"/"minor"), contains(pitch), pitch_classes() |
CounterpointGenerator |
generate(), generate_n_voices(n, ranges) |
Tensor-MIDI Output (counterpoint_engine.tensor_output)
from counterpoint_engine.tensor_output import (
voices_to_tensor_events, voice_leading_to_sidechannels,
interval_to_flux_vector, voice_intervals_to_flux_vectors,
TensorMIDIEvent
)
tensor_events, midi_events = voices_to_tensor_events(voices)
raw = tensor_events[0].to_bytes() # 4 bytes: cos, sin, beat, state
gestures = voice_leading_to_sidechannels(voices, beat=2)
# {(0,1): "Smile", (0,2): "Nod", (1,2): "Frown"}
fv = interval_to_flux_vector(7) # perfect fifth โ FluxVector
| Function | Returns |
|---|---|
voices_to_tensor_events(voices) |
(List[TensorMIDIEvent], List[MidiEvent]) |
voice_leading_to_sidechannels(voices, beat) |
Dict[(i,j), str] โ Nod/Smile/Frown |
interval_to_flux_vector(interval) |
FluxVector via Aโ lattice |
voice_intervals_to_flux_vectors(voices, beat) |
List[FluxVector] |
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ counterpoint-engine โ
โ โ
โ rules.py laman_counterpoint.py โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โ
โ โ SAT/UNSATโโโโโโโโ CounterpointGraphโ โ
โ โ kernels โ โ henneberg_constructโ โ
โ โโโโโโฌโโโโโโ โโโโโโโโโโฌโโโโโโโโโโ โ
โ โ โ โ
โ โผ โผ โ
โ generator.py โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ CounterpointGenerator โ โ
โ โ .generate() โ List[int] โ โ
โ โ .generate_n_voices() โ voices โ โ
โ โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ tensor_output.py โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ voices_to_tensor_events() โ โ
โ โ voice_leading_to_sidechannels() โ โ
โ โ interval_to_flux_vector() โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Dependencies โ
โ constraint-theory-core โ Laman rigidity, Aโ latticeโ
โ flux-tensor-midi โ FluxVector, MidiEvent types โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Data flow: cantus firmus โ generator (backtracking) โ voices โ tensor_output โ TensorMIDIEvent stream
Documentation
- User Guide โ Complete usage documentation
- Developer Guide โ Contributing and internals
- Examples โ Working code examples
Ecosystem
- constraint-theory-core โ Laman rigidity, Aโ lattice, dodecet directions
- flux-tensor-midi โ FluxVector, MidiEvent, tensor-midi event stream
- plato-room-musician โ Music theory room in the PLATO knowledge system
Requirements
- Python โฅ 3.10
constraint-theory-core(from../constraint-theory-core, add toPYTHONPATH)flux-tensor-midi(Tensor-MIDI event types)
Installation
pip install counterpoint-engine
Or install from source with dependencies:
pip install constraint-theory-core flux-tensor-midi
git clone https://github.com/SuperInstance/counterpoint-engine.git
cd counterpoint-engine
pip install -e ".[dev]"
pytest
Status
All 78 tests pass.
License
Apache 2.0
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file counterpoint_engine-0.3.0.tar.gz.
File metadata
- Download URL: counterpoint_engine-0.3.0.tar.gz
- Upload date:
- Size: 52.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b57ca8db48590a467396d1f7e2cb9b9616f61d1769e7d58860fa85551d14b7df
|
|
| MD5 |
791065b1a00ed6a9e7ce9a2c2ebb4d0d
|
|
| BLAKE2b-256 |
effee395e17ac07dd4dfe1c34498a0a287f55e75fc2d36c429cf8fbfefc1dde3
|
File details
Details for the file counterpoint_engine-0.3.0-py3-none-any.whl.
File metadata
- Download URL: counterpoint_engine-0.3.0-py3-none-any.whl
- Upload date:
- Size: 41.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
665f634faa8795724f345b193c7272e1d2a4317e02e40d5404e42624a27cad96
|
|
| MD5 |
c8d4f5a1302cc8a30dc782d6efdb10be
|
|
| BLAKE2b-256 |
0c0e8b332c1d6243a877e438bd6223ff3d9f710338b84415e517b571c094fe6f
|