"""Regular constraints shared by generation engines.""" from __future__ import annotations from collections.abc import Iterable from .common import Symbol, duration_units, is_triplet_duration class ThemeGenerationAcceptor: """Dense weighted DFA for endpoint weights and rhythmic phase constraints.""" rejected_state = -1 def __init__( self, *, length: int, alphabet: Iterable[Symbol], start_weights: dict[int, float], end_weights: dict[int, float], strength: float, enforce_triplet_groups: bool, ) -> None: self.length = length self.start_weights = start_weights self.end_weights = end_weights self.strength = strength self.enforce_triplet_groups = enforce_triplet_groups self.name = "theme_generation_dense" alphabet_tuple = tuple(alphabet) self.alphabet = frozenset(alphabet_tuple) self.start_state = self.encode_state(0, 0, 0) self.state_count_value = (length + 1) * 12 * 3 self.states = frozenset(range(self.state_count_value)) self.accept_states = frozenset(self.encode_state(length, phase, 0) for phase in range(12)) self.dense_transition_by_symbol = { symbol: self.transition_column(symbol) for symbol in alphabet_tuple } @staticmethod def encode_state(position: int, phase: int, triplets_to_close: int) -> int: return ((position * 12) + phase) * 3 + triplets_to_close @staticmethod def decode_state(state: int) -> tuple[int, int, int]: position_phase, triplets_to_close = divmod(state, 3) position, phase = divmod(position_phase, 12) return position, phase, triplets_to_close def transition_column(self, symbol: Symbol) -> tuple[int, ...]: return tuple(self.next_state_for_symbol(state, symbol) for state in self.states) def next_state_for_symbol(self, state: int, symbol: Symbol) -> int: position, phase, triplets_to_close = self.decode_state(state) if position >= self.length: return self.rejected_state units = duration_units(symbol.duration) is_triplet = is_triplet_duration(symbol.duration) next_triplets_to_close = 0 if self.enforce_triplet_groups: if triplets_to_close > 0: if not is_triplet: return self.rejected_state next_triplets_to_close = triplets_to_close - 1 elif is_triplet: if phase != 0: return self.rejected_state next_triplets_to_close = 2 return self.encode_state( position + 1, (phase + units) % 12, next_triplets_to_close, ) def next_state(self, state: int, symbol: Symbol) -> int | None: transitions = self.dense_transition_by_symbol.get(symbol) if transitions is None: return None next_state = transitions[state] if next_state == self.rejected_state: return None return next_state def is_accepting(self, state: int) -> bool: return state in self.accept_states def transition_weight(self, state: int, symbol: Symbol) -> float: position, _, _ = self.decode_state(state) if position == 0: return self.start_weights.get(symbol.rpc, 1.0) ** self.strength if position == self.length - 1: return self.end_weights.get(symbol.rpc, 1.0) ** self.strength return 1.0 def state_count(self) -> int: return self.state_count_value def make_theme_acceptor( *, length: int, alphabet: Iterable[Symbol], start_weights: dict[int, float], end_weights: dict[int, float], strength: float, enforce_triplet_groups: bool, ) -> ThemeGenerationAcceptor: return ThemeGenerationAcceptor( length=length, alphabet=alphabet, start_weights=start_weights, end_weights=end_weights, strength=strength, enforce_triplet_groups=enforce_triplet_groups, )