Skip to content

Instantly share code, notes, and snippets.

@arseniiv
Created February 18, 2024 23:25
Show Gist options
  • Select an option

  • Save arseniiv/5aaa69f638c8197a1551b9600858d524 to your computer and use it in GitHub Desktop.

Select an option

Save arseniiv/5aaa69f638c8197a1551b9600858d524 to your computer and use it in GitHub Desktop.
Sorting subdivided scales by avg variety, filtering out rotated variants
from __future__ import annotations
from collections import defaultdict
from typing import Final, Iterable, Iterator, Mapping, Sequence
import operator
from dataclasses import dataclass
from itertools import accumulate, permutations, product
from math import prod
from fractions import Fraction as F
def avg_variety(scale_steps: Sequence[F]) -> float:
if not scale_steps:
raise ValueError('scale should be nonempty')
if len(scale_steps) == 1:
return 1
last_row = scale_steps
varieties = [len(set(last_row))]
scale_steps = list(scale_steps)
for _ in range(len(scale_steps) - 1):
scale_steps.append(scale_steps.pop(0))
last_row = [step + interval
for step, interval in zip(scale_steps, last_row, strict=True)]
varieties.append(len(set(last_row)))
assert varieties[-1] == 1
varieties.pop()
return sum(varieties) / len(varieties)
@dataclass(frozen=True)
class Task:
macro_stepwise_scale: Sequence[F]
rules: Mapping[F, Sequence[F]]
def __post_init__(self) -> None:
for val, shards in self.rules.items():
if val != prod(shards):
raise ValueError(f'a rule has {shards} not stacking to {val}')
def replaced_stepwise_scales(self) -> Iterator[tuple[F, ...]]:
replacement_pools = tuple(
tuple(permutations(self.rules.get(step, [step])))
for step in self.macro_stepwise_scale)
replacement: tuple[tuple[F, ...], ...]
for replacement in product(*replacement_pools):
yield tuple(step for group in replacement for step in group)
def show_scale_scores(stepwise_scales: Iterable[Sequence[F]],
show_count: int) -> None:
entries_d = defaultdict(list)
avg_var_sum = 0
for scale in stepwise_scales:
avg_var = avg_variety(scale)
rot_helper = str(' '.join(map(str, scale)))
is_rotation = False
for _, other_scale_rot_helper in entries_d[avg_var]:
if rot_helper in other_scale_rot_helper:
is_rotation = True
break
if is_rotation:
continue
rot_helper = rot_helper + ' ' + rot_helper
entries_d[avg_var].append((scale, rot_helper))
avg_var_sum += avg_var
entries = [(avg_var, scale) for avg_var, scales in entries_d.items()
for (scale, _) in scales]
entries.sort()
print(f'showing first {show_count} of total {len(entries)} scales')
for avg_var, scale in entries[:show_count]:
cum_scale = accumulate(scale, func=operator.mul)
print(f'avg variety: {avg_var:.3f}')
print(' '.join(map(str, cum_scale)), '\n')
print('avg variety stats:')
print(f' smallest: {entries[0][0]:.3f}')
print(f' average: {avg_var_sum / len(entries):.3f}')
print(f' largest: {entries[-1][0]:.3f}')
def ptolemy_soft_diatonic_into_three() -> Task:
L1: Final = F('8/7')
L2: Final = F('9/8')
L3: Final = F('10/9')
S: Final = F('21/20')
assert L1 > L2 > L3 > S
INIT_SCALE_STEPS: Final = (L3, L1, S, L3, L1, L2, S)
assert prod(INIT_SCALE_STEPS) == 2
RULES: Final = {
L1: tuple(F(s) for s in '24/23 23/22 22/21'.split()),
L2: tuple(F(s) for s in '27/26 26/25 25/24'.split()),
L3: tuple(F(s) for s in '30/29 29/28 28/27'.split()),
}
return Task(INIT_SCALE_STEPS, RULES)
def ptolemy_soft_diatonic_into_two() -> Task:
L1: Final = F('8/7')
L2: Final = F('9/8')
L3: Final = F('10/9')
S: Final = F('21/20')
assert L1 > L2 > L3 > S
INIT_SCALE_STEPS: Final = (L3, L1, S, L3, L1, L2, S)
assert prod(INIT_SCALE_STEPS) == 2
RULES: Final = {
L1: tuple(F(s) for s in '16/15 15/14'.split()),
L2: tuple(F(s) for s in '18/17 17/16'.split()),
L3: tuple(F(s) for s in '20/19 19/18'.split()),
}
return Task(INIT_SCALE_STEPS, RULES)
def zarlino_into_three() -> Task:
L: Final = F('9/8')
M: Final = F('10/9')
S: Final = F('16/15')
assert L > M > S
INIT_SCALE_STEPS: Final = (L, M, S, L, M, L, S)
assert prod(INIT_SCALE_STEPS) == 2
RULES: Final = {
L: tuple(F(s) for s in '135/128 256/243 81/80'.split()),
M: tuple(F(s) for s in '135/128 256/243'.split()),
S: tuple(F(s) for s in '256/243 81/80'.split()),
}
return Task(INIT_SCALE_STEPS, RULES)
def main() -> None:
task = zarlino_into_three()
scales = task.replaced_stepwise_scales()
show_scale_scores(scales, 10)
if __name__ == '__main__':
main()
@arseniiv

Copy link
Copy Markdown
Author

Three examples are given: ptolemy_soft_diatonic_into_three, ptolemy_soft_diatonic_into_two, zarlino_into_three.

Specify a scale by its steps, make rules of partitioning each step into more steps. If they don’t stack into the original size, you’ll be notified.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment