Skip to main content

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

Ecosystem

Requirements

  • Python ≥ 3.10
  • constraint-theory-core (from ../constraint-theory-core, add to PYTHONPATH)
  • 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

Tests Version License

All 78 tests pass.

License

Apache 2.0

Metadata

Release files for counterpoint-engine 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for counterpoint-engine 0.3.0
File Size Uploaded Provenance Actions
counterpoint_engine-0.3.0.tar.gz 52.1 kB Without provenance Details for counterpoint_engine-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for counterpoint-engine 0.3.0
File Interpreter ABI Platform Actions
counterpoint_engine-0.3.0-py3-none-any.whl Python 3 none any Details for counterpoint_engine-0.3.0-py3-none-any.whl

Total release size: 94.0 kB

Release files / counterpoint_engine-0.3.0.tar.gz

Download URL counterpoint_engine-0.3.0.tar.gz
Size 52.1 kB
Tags Source
SHA-256 checksum
How to use checksums
b57ca8db48590a467396d1f7e2cb9b9616f61d1769e7d58860fa85551d14b7df
BLAKE2b-256 checksum
How to use checksums
effee395e17ac07dd4dfe1c34498a0a287f55e75fc2d36c429cf8fbfefc1dde3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.12

Release files / counterpoint_engine-0.3.0-py3-none-any.whl

Download URL counterpoint_engine-0.3.0-py3-none-any.whl
Size 41.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
665f634faa8795724f345b193c7272e1d2a4317e02e40d5404e42624a27cad96
BLAKE2b-256 checksum
How to use checksums
0c0e8b332c1d6243a877e438bd6223ff3d9f710338b84415e517b571c094fe6f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.10.12

Security for counterpoint-engine 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page