Source code for klotho.tonos.systems.harmonic_trees.harmonic_tree

from klotho.topos.graphs import Tree
from .algorithms import *
from klotho.tonos.utils.interval_normalization import reduce_interval
from typing import Tuple, Union
from fractions import Fraction


def _odd_prime_generator():
    yield 3
    primes = [3]
    n = 5
    while True:
        if all(n % p != 0 for p in primes if p * p <= n):
            primes.append(n)
            yield n
        n += 2


[docs] class HarmonicTree(Tree): _node_value_attr = 'factor' """ A tree structure that models multiplicative harmonic relationships. Each leaf node's *harmonic* value is the product of node factors along the path from the root. When an equave is specified, leaf ratios are reduced into the equave window controlled by *span*. This is useful for building spectra, combination tones, and other structures derived from a chain of harmonic multiplications. Parameters ---------- root : int, optional The factor of the root node (typically the fundamental partial number). Default is 1. children : tuple of int, optional Child factors that define the branching structure. Default is ``(1,)``. equave : Fraction, int, float, str, or None, optional Interval of equivalence for ratio reduction. ``None`` disables reduction. span : int, optional Number of equaves for the reduction window. Default is 1. """
[docs] def __init__(self, root:int = 1, children:Tuple[int, ...] = (1,), equave:Union[Fraction,int,float,str,None] = None, span:int = 1): super().__init__(root, children) self._meta['equave'] = Fraction(equave) if equave is not None else None self._meta['span'] = span self._evaluate()
def _post_structure_clone(self): self._meta['equave'] = None self._meta['span'] = 1 non_root = [n for n in self._graph.node_indices() if n != self._root] nodes_by_depth = sorted(non_root, key=lambda n: self.depth_of(n), reverse=True) odd_primes = _odd_prime_generator() factor_map = {n: next(odd_primes) for n in nodes_by_depth} self._graph[self._root] = {'factor': 1} for node in non_root: self._graph[node] = {'factor': factor_map[node]} self._evaluate() def _normalize_node_attrs(self, node, attrs, op=None): normalized = dict(attrs) if isinstance(attrs, dict) else {} if 'label' in normalized: raise ValueError("HarmonicTree does not accept 'label'; use 'factor'") return normalized def _validate_node_attrs(self, node, attrs, op=None): mutable_keys = {'factor'} illegal = [k for k in attrs if k not in mutable_keys] if illegal: raise ValueError(f"Illegal HarmonicTree node attribute update: {illegal}") if 'multiple' in attrs or 'harmonic' in attrs or 'ratio' in attrs: raise ValueError("multiple, harmonic, and ratio are derived and cannot be set directly") def _resolve_data_update_scope(self, node, changed_keys, op=None): if 'factor' in changed_keys: return node return super()._resolve_data_update_scope(node=node, changed_keys=changed_keys, op=op) def _evaluate(self, root_node=None): if root_node is None: root_node = self.root if root_node == self.root: parent_harmonic = 1 else: parent = self.parent(root_node) if parent is None or 'harmonic' not in self[parent]: self._evaluate(self.root) return parent_harmonic = self[parent]['harmonic'] def process_subtree(node, inherited_harmonic): value = self[node]['factor'] harmonic = value * inherited_harmonic self._graph[node]['multiple'] = value if value > 0 else Fraction(1, abs(value)) self._graph[node]['harmonic'] = harmonic if self.equave is not None: self._graph[node]['ratio'] = reduce_interval(Fraction(harmonic), self.equave, self.span) else: self._graph[node]['ratio'] = harmonic for child in self.successors(node): process_subtree(child, harmonic) process_subtree(root_node, parent_harmonic) def _after_post_mutation(self, scope_node=None, op=None): self._evaluate(scope_node) @property def harmonics(self): """tuple : Raw harmonic values at each leaf node (product along path from root).""" return tuple(self[n]['harmonic'] for n in self.leaf_nodes) @property def ratios(self): """tuple : Equave-reduced ratios at each leaf node (or raw harmonics if no equave).""" return tuple(self[n]['ratio'] for n in self.leaf_nodes) @property def equave(self): """Fraction or None : The interval of equivalence, or None if reduction is disabled.""" return self._meta['equave'] @property def span(self): """int : Number of equaves used for the reduction window.""" return self._meta['span']
# @property # def inverse(self): # return HarmonicTree( # root = self.__root, # children = self.__children, # equave = self.__equave, # span = self.__span, # inverse = not self.__inverse # )