from klotho import Tree, RhythmTree as RT, plot, play
from klotho.topos import PartitionSet as PS
from fractions import Fraction
import numpy as npBasic Intuitions¶
PS(8, 3)------------------------------------------
PS(n=8, k=3) - Graph: feature_distance
Mean: ~2.6667
------------------------------------------
partition unique_count span variance
0 (6, 1, 1) 2 5 5.5556
1 (5, 2, 1) 3 4 2.8889
2 (4, 3, 1) 3 3 1.5556
3 (4, 2, 2) 2 2 0.8889
4 (3, 3, 2) 2 1 0.2222
------------------------------------------PS(n, k) generates all the ways to partition the integer n into exactly k parts. The table is sorted from most uneven (highest variance) to most even (lowest variance).
So PS(8, 3) answers the question: what are all the ways to divide 8 beats into 3 groups?
Let’s hear what each partition sounds like as a rhythm:
Common Patterns¶
for p in PS(8, 3).partitions:
rt = RT(subdivisions=p)
print(f"S = {p}")
print(f"{' + '.join(str(f) for f in rt.durations)} = {sum(rt.durations)}")
plot(rt, layout='ratios', animate=True, beat='1/4', bpm=126)
print()S = (6, 1, 1)
3/4 + 1/8 + 1/8 = 1
S = (5, 2, 1)
5/8 + 1/4 + 1/8 = 1
S = (4, 3, 1)
1/2 + 3/8 + 1/8 = 1
S = (4, 2, 2)
1/2 + 1/4 + 1/4 = 1
S = (3, 3, 2)
3/8 + 3/8 + 1/4 = 1
Notice the last partition, (3, 3, 2). This is the most even way to divide 8 into 3. If you’ve studied rhythm, you might recognize it — this is the classic 3-3-2 clave pattern, one of the most common rhythmic cells in Afro-Cuban, jazz, and popular music.
It’s not a coincidence that this pattern feels so “right”. It’s the partition of 8 into 3 groups that is closest to equal — the lowest possible variance.
rt = RT(subdivisions=(3, 3, 2))
plot(rt, layout='ratios', animate=True, beat='1/4', bpm=126)Beyond powers of 2¶
PS(8, 3) partitions 8 — and 8 is a very “standard” number of beats. It maps cleanly onto 8th notes in 4/4 time. The same goes for 4, 16, 32, etc. These are familiar subdivisions of familiar meters.
Things get more interesting when n is not a power of 2. What are all the ways to divide 5 beats into 2 groups? Or 7 into 3? Or 11 into 4?
PS(5, 2)-----------------------------------------
PS(n=5, k=2) - Graph: feature_distance
Mean: ~2.5
-----------------------------------------
partition unique_count span variance
0 (4, 1) 2 3 2.25
1 (3, 2) 2 1 0.25
-----------------------------------------for p in PS(5, 2).partitions:
rt = RT(subdivisions=p)
print(f"S = {p}")
print(f"{' + '.join(str(f) for f in rt.durations)} = {sum(rt.durations)}")
plot(rt, layout='ratios', animate=True, beat='1/4', bpm=126)
print()S = (4, 1)
4/5 + 1/5 = 1
S = (3, 2)
3/5 + 2/5 = 1
Only two options, and neither is equal — 5 can’t be evenly divided into 2. The most balanced partition is (3, 2), and you can hear it: a slightly lopsided pair. This is the basic feel of 5/8 time.
PS(7, 3)------------------------------------------
PS(n=7, k=3) - Graph: feature_distance
Mean: ~2.3333
------------------------------------------
partition unique_count span variance
0 (5, 1, 1) 2 4 3.5556
1 (4, 2, 1) 3 3 1.5556
2 (3, 3, 1) 2 2 0.8889
3 (3, 2, 2) 2 1 0.2222
------------------------------------------for p in PS(7, 3).partitions:
rt = RT(subdivisions=p)
print(f"S = {p}")
print(f"{' + '.join(str(f) for f in rt.durations)} = {sum(rt.durations)}")
plot(rt, layout='ratios', animate=True, beat='1/4', bpm=126)
print()S = (5, 1, 1)
5/7 + 1/7 + 1/7 = 1
S = (4, 2, 1)
4/7 + 2/7 + 1/7 = 1
S = (3, 3, 1)
3/7 + 3/7 + 1/7 = 1
S = (3, 2, 2)
3/7 + 2/7 + 2/7 = 1
7 into 3 gives us four options. The most balanced, (3, 2, 2), is a common 7/8 grouping — and again, it’s not quite even. No partition of 7 into 3 can be. That inherent asymmetry is what gives odd meters their characteristic feel.
Let’s push further:
PS(11, 4)----------------------------------------------
PS(n=11, k=4) - Graph: feature_distance
Mean: ~2.75
----------------------------------------------
partition unique_count span variance
0 (8, 1, 1, 1) 2 7 9.1875
1 (7, 2, 1, 1) 3 6 6.1875
2 (6, 3, 1, 1) 3 5 4.1875
3 (6, 2, 2, 1) 3 5 3.6875
4 (5, 4, 1, 1) 3 4 3.1875
5 (5, 3, 2, 1) 4 4 2.1875
6 (5, 2, 2, 2) 2 3 1.6875
7 (4, 4, 2, 1) 3 3 1.6875
8 (4, 3, 3, 1) 3 3 1.1875
9 (4, 3, 2, 2) 3 2 0.6875
10 (3, 3, 3, 2) 2 1 0.1875
----------------------------------------------for p in PS(11, 4).partitions:
rt = RT(subdivisions=p)
print(f"S = {p}")
plot(rt, layout='ratios', animate=True, beat='1/4', bpm=126)
print()S = (8, 1, 1, 1)
S = (7, 2, 1, 1)
S = (6, 3, 1, 1)
S = (6, 2, 2, 1)
S = (5, 4, 1, 1)
S = (5, 3, 2, 1)
S = (5, 2, 2, 2)
S = (4, 4, 2, 1)
S = (4, 3, 3, 1)
S = (4, 3, 2, 2)
S = (3, 3, 3, 2)
11 into 4 gives us 11 partitions — a much wider range of rhythmic options. Listen to the progression from top to bottom: the first partition (8, 1, 1, 1) is extremely lopsided, while the last (3, 3, 3, 2) is as close to even as 11 will allow. The partitions in between trace a smooth gradient from uneven to balanced.
This is the real utility of PS: it gives us the complete set of rhythmic groupings for a given n and k, organized by their structural properties. We don’t have to guess — we can see all the options and choose.
We can also ask: how does the number of groups k affect things for the same n?
for k in range(2, 5):
ps_k = PS(13, k)
print(f"\nPS(13, {k}) — {len(ps_k.partitions)} partitions:")
for p in ps_k.partitions:
rt = RT(subdivisions=p)
plot(rt, layout='ratios', animate=True, beat='1/4', bpm=126)
PS(13, 2) — 6 partitions:
PS(13, 3) — 14 partitions:
PS(13, 4) — 18 partitions:
A few things to notice:
Small
kmeans fewer, longer groups — the rhythm is “chunkier”.Large
kmeans many, shorter groups — the rhythm approaches a pulse.For any given
k, the most balanced partition (lowest variance) is always the one closest to dividingnevenly.
Filtering Choices¶
Permutations¶
Subdivisions¶
Nesting¶