compact_sets/src/pitch.py
Michael Winter 0698d01d85 Refactor into src/ module with caching and CLI improvements
- Split monolithic compact_sets.py into modular src/ directory
- Add graph caching (pickle + JSON) with --cache-dir option
- Add --output-dir, --rebuild-cache, --no-cache CLI options
- Default seed is now None (random) instead of 42
- Add .gitignore entries for cache/ and output/
2026-03-13 18:38:38 +01:00

93 lines
2.6 KiB
Python

#!/usr/bin/env python
"""
Pitch class - a point in harmonic space.
Represented as an array of exponents on prime dimensions.
Example: (0, 1, 0, 0) represents 3/2 (perfect fifth) in CHS_7
"""
from __future__ import annotations
from fractions import Fraction
from math import log
from operator import add
from typing import Iterator
DIMS_8 = (2, 3, 5, 7, 11, 13, 17, 19)
DIMS_7 = (2, 3, 5, 7, 11, 13, 17)
DIMS_5 = (2, 3, 5, 7, 11)
DIMS_4 = (2, 3, 5, 7)
class Pitch:
def __init__(self, hs_array: tuple[int, ...], dims: tuple[int, ...] | None = None):
self.hs_array = hs_array
self.dims = dims if dims is not None else DIMS_7
def __hash__(self) -> int:
return hash(self.hs_array)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Pitch):
return NotImplemented
return self.hs_array == other.hs_array
def __repr__(self) -> str:
return f"Pitch({self.hs_array})"
def __iter__(self):
return iter(self.hs_array)
def __len__(self) -> int:
return len(self.hs_array)
def __getitem__(self, index: int) -> int:
return self.hs_array[index]
def to_fraction(self) -> Fraction:
"""Convert to frequency ratio (e.g., 3/2)."""
from math import prod
return Fraction(
prod(pow(self.dims[d], self.hs_array[d]) for d in range(len(self.dims)))
)
def to_cents(self) -> float:
"""Convert to cents (relative to 1/1 = 0 cents)."""
fr = self.to_fraction()
return 1200 * log(float(fr), 2)
def collapse(self) -> Pitch:
"""
Collapse pitch so frequency ratio is in [1, 2).
This removes octave information, useful for pitch classes.
"""
collapsed = list(self.hs_array)
fr = self.to_fraction()
if fr < 1:
while fr < 1:
fr *= 2
collapsed[0] += 1
elif fr >= 2:
while fr >= 2:
fr /= 2
collapsed[0] -= 1
return Pitch(tuple(collapsed), self.dims)
def project(self) -> Pitch:
"""Project pitch to [1, 2) range - same as collapse."""
return self.collapse()
def transpose(self, trans: Pitch) -> Pitch:
"""Transpose by another pitch (add exponents element-wise)."""
return Pitch(tuple(map(add, self.hs_array, trans.hs_array)), self.dims)
def pitch_difference(self, other: Pitch) -> Pitch:
"""Calculate the pitch difference (self - other)."""
return Pitch(
tuple(self.hs_array[d] - other.hs_array[d] for d in range(len(self.dims))),
self.dims,
)