Created
February 18, 2024 23:25
-
-
Save arseniiv/5aaa69f638c8197a1551b9600858d524 to your computer and use it in GitHub Desktop.
Sorting subdivided scales by avg variety, filtering out rotated variants
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.