pachet's picture
Deploy Theme Lab Docker app
187bf9a
Raw
History Blame Contribute Delete
4.12 kB
"""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,
)