Source code for klotho.topos.collections.sequences

from itertools import cycle, combinations
from typing import List, Optional, Union, Any, TypeVar

import numpy as np

from klotho.topos.collections._pattern import (
    Cyclic,
    NodeSpec,
    _advance_runtime,
    _build_runtime,
    _compile_item,
    _reset_runtime,
    child_restore,
    child_snapshot,
)

T = TypeVar('T')

__all__ = [
    'Norg',
    'Pattern',
    'Cyclic',
]

[docs] class Norg: """Per Nørgård's "infinity" sequences. Provides static methods for generating various integer sequences devised by the Danish composer Per Nørgård, most notably the *infinity series* used extensively in his compositional practice. References ---------- .. [1] https://web.archive.org/web/20071010091253/http://www.pernoergaard.dk/eng/strukturer/uendelig/uindhold.html Examples -------- >>> Norg.inf(size=8) array([ 0, 1, -1, 2, 1, 0, -2, 3]) """
[docs] @staticmethod def inf(start: int = 0, size: int = 128, step: int = 1): """Generate the infinity series. When *start* is 0 and *step* is 1 an optimised pair-wise recurrence is used. For arbitrary start/step combinations the series is computed element-wise via `inf_num`. Parameters ---------- start : int, optional Index at which to begin the series (default is 0). size : int, optional Number of elements to generate (default is 128). step : int, optional Index step between successive elements (default is 1). Returns ------- numpy.ndarray One-dimensional integer array of length *size*. References ---------- .. [1] https://web.archive.org/web/20071010092334/http://www.pernoergaard.dk/eng/strukturer/uendelig/ukonstruktion05.html Examples -------- >>> Norg.inf(size=8) array([ 0, 1, -1, 2, 1, 0, -2, 3]) >>> Norg.inf(start=4, size=4) array([1, 0, -2, 3]) """ if start == 0 and step == 1: p = np.empty(size, dtype=int) p[0] = 0 p[1] = 1 for i in range(1, (size - 1) // 2 + 1): delta = p[i] - p[i - 1] if 2 * i < size: p[2 * i] = p[2 * i - 2] - delta if 2 * i + 1 < size: p[2 * i + 1] = p[2 * i - 1] + delta return p return np.array([Norg.inf_num(start + step * i) for i in range(size)])
[docs] @staticmethod def inf_num(n): """Compute the *n*-th value of the infinity series. Uses the recursive definition: ``f(0) = 0``, ``f(2n) = -f(n)``, ``f(2n+1) = f(n) + 1``. Parameters ---------- n : int Non-negative index into the infinity series. Returns ------- int The value of the infinity series at position *n*. References ---------- .. [1] https://arxiv.org/pdf/1402.3091.pdf Examples -------- >>> Norg.inf_num(0) 0 >>> Norg.inf_num(7) 3 """ if n == 0: return 0 if n % 2 == 0: return -Norg.inf_num(n // 2) return Norg.inf_num((n - 1) // 2) + 1
[docs] @staticmethod def n_partite(seed: list = [0,-2,-1], inv_pat: list = [-1,1,1], size: int = 128): """Generate a generalised *n*-partite infinity series. Extends the tripartite construction to an arbitrary seed length and inversion pattern, producing self-similar integer sequences by recursively applying delta-inversion rules. Parameters ---------- seed : list of int, optional Initial values of the series (default is ``[0, -2, -1]``). inv_pat : list of int, optional Cyclic pattern of inversion multipliers applied to successive deltas (default is ``[-1, 1, 1]``). size : int, optional Number of elements to generate (default is 128). Returns ------- numpy.ndarray One-dimensional integer array of length *size*. References ---------- .. [1] https://web.archive.org/web/20071010091606/http://www.pernoergaard.dk/eng/strukturer/uendelig/u3.html Examples -------- >>> Norg.n_partite(size=9) array([ 0, -2, -1, 2, 0, -1, -2, 0, 1]) """ seed_len = len(seed) p = np.empty(size, dtype=int) p[:seed_len] = seed inv_pat_cycle = cycle(inv_pat) for i in range(1, (size - 1) // seed_len + 1): delta = p[i] - p[i - 1] for j in range(seed_len): if seed_len * i + j < size: p[seed_len * i + j] = p[seed_len * i + j - seed_len] + next(inv_pat_cycle) * delta return p
[docs] @staticmethod def lake(): """Generate Nørgård's *tone lake* sequence. .. note:: Not yet implemented. Returns ------- None References ---------- .. [1] https://web.archive.org/web/20071010093955/http://www.pernoergaard.dk/eng/strukturer/toneso/tkonstruktion.html """ pass
[docs] class Pattern: """Cyclical pattern iterator built from nested structure and delegates."""
[docs] def __init__(self, iterable, end=False): self._iterable = iterable self._end = end self._spec = _compile_item(iterable) self._pattern_length = self._spec.period self._root = _build_runtime(self._spec, iterable) self._current = 0 self._period_cache: Optional[tuple[Any, ...]] = None
@property def length(self) -> int: return self._pattern_length @property def spec(self) -> NodeSpec: return self._spec @property def pattern(self): return self._iterable @property def end(self): return self._end @property def position(self) -> int: return self._current
[docs] def reset(self): self._current = 0 _reset_runtime(self._root) self._period_cache = None
[docs] def materialize_period(self) -> tuple[Any, ...]: if self._period_cache is None: snap = self._snapshot() try: self._period_cache = tuple(self._next_value() for _ in range(self._pattern_length)) finally: self._restore(snap) return self._period_cache
def _snapshot(self): return { 'current': self._current, 'root': child_snapshot(self._root), } def _restore(self, state): self._current = state['current'] child_restore(self._root, state['root']) def _next_value(self): if self._current >= self._pattern_length and self._end: return next(self._end) if isinstance(self._end, Pattern) else self._end value = _advance_runtime(self._root) self._current += 1 return value def __iter__(self): return self def __next__(self): return self._next_value() def __len__(self): return self._pattern_length def __str__(self): return str(list(self._iterable)) if isinstance(self._iterable, list) else str(self._iterable) def __repr__(self): return f"Pattern({self._iterable!r}, end={self._end!r})"
[docs] @staticmethod def from_random( elements: List[T], length: int = 5, max_nesting_level: int = 3, max_inner_length: int = 3, weights: Optional[List[float]] = None, nesting_probability: float = 0.333, ) -> 'Pattern': from klotho.utils.algorithms.lists import normalize_sum if weights is not None: if len(weights) != len(elements): raise ValueError("Length of weights must match length of elements") normalized_weights = normalize_sum(weights) else: normalized_weights = [1.0 / len(elements)] * len(elements) def _generate_structure(target_length: int, current_nesting_level: int) -> List[Union[T, List[Any]]]: structure = [] for _ in range(target_length): if max_inner_length > 0 and current_nesting_level > 0 and np.random.random() < nesting_probability: nested_length = np.random.randint(2, max_inner_length + 1) nested_structure = _generate_structure(nested_length, current_nesting_level - 1) structure.append(nested_structure) else: index = np.random.choice(len(elements), p=normalized_weights) structure.append(elements[index]) return structure return Pattern(_generate_structure(length, max_nesting_level))
# class Golomb: # def __init__(self, length: int, order: int, *, reflections=False): # self._length = length # self._order = order # self._reflections = reflections # self._rulers = self._golomb_rulers_of_length(length, order, reflections=reflections) # def _is_golomb_ruler(marks): # diffs = set() # for i in range(len(marks)): # for j in range(i + 1, len(marks)): # d = marks[j] - marks[i] # if d in diffs: # return False # diffs.add(d) # return True # def _golomb_rulers_of_length(length, order, *, reflections=True): # if order < 1: # raise ValueError("order must be >= 1") # if order == 1: # return [[0]] if length == 0 else [] # if order == 2: # return [[0, length]] if length > 0 else [] # rulers = [] # for interior in combinations(range(1, length), order - 2): # marks = [0, *interior, length] # if not _is_golomb_ruler(marks): # continue # if not reflections: # reflected = [length - x for x in reversed(marks)] # if marks > reflected: # continue # rulers.append(marks) # return rulers