Files
product-vamp/generator/solo/construction.py
T
2026-10-05 17:24:16 -05:00

105 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Bounded comparison of completed source-related phrases."""
import collections
import dataclasses
import fractions
import time
import generator.solo.phrases
import generator.solo.selection
import generator.solo.sequences
import generator.solo.policy
import generator.vocabulary
class SearchExhausted(ValueError):
pass
@dataclasses.dataclass(frozen=True)
class State:
phrases: tuple = ()
home: tuple = ()
total: fractions.Fraction = fractions.Fraction()
@property
def notes(self):
return tuple(n for p in self.phrases for n in p.notes)
class Builder:
def __init__(self,analysis,measures,harmony,sequences,rhythms,end,seed,enabled=True,*,start=fractions.Fraction()):
self.analysis,self.harmony = analysis,harmony
self.start,self.end,self.seed = start,end,seed
self.phrases = generator.solo.phrases.Phrases(analysis,harmony,sequences,rhythms,start,end,seed,enabled)
self.rules = generator.vocabulary.data('solo')['search']
self.timings = {}; self.frontiers = []; self.coverage = collections.defaultdict(collections.Counter); self.decisions = []
self.failures = self.phrases.rejections
self.opening_candidates = []
def candidates(self,start,end,state):
incoming = state.notes[-1] if state.notes else None
values = {}
for variant in range(self.rules['alternatives']):
value = self.phrases.make(start,end,incoming,state.home,state.phrases,variant)
if value is not None:
phrase,introduced = value
self.coverage[phrase.purpose]['valid'] += 1
key = tuple((n.start,n.end,n.pitch.number()) for n in phrase.notes)
values.setdefault(key,value)
for phrase,_ in values.values():
self.coverage[phrase.purpose]['distinct'] += 1
return tuple(values.values())
def rank(self,state):
return state.total,generator.solo.policy.stable_rank(self.seed,tuple((n.start,n.end,n.pitch.number()) for n in state.notes))
@staticmethod
def character(state):
phrase = state.phrases[-1]
ns = phrase.notes
# Diversity describes the realized phrase, not its constructor label.
return (tuple((n.start-phrase.start,n.end-n.start) for n in ns[:4]),
tuple((b.pitch.number()>a.pitch.number())-(b.pitch.number()<a.pitch.number()) for a,b in zip(ns[:3],ns[1:4])),
tuple((a.earliest%1,a.effect) for a in phrase.arrivals))
def compose(self):
began = time.monotonic(); self.frontiers = []; self.decisions = []
openings = []
states = (State(),); start = self.start
while start<self.end:
end = self.phrases.horizon(start)
successors = []
for state in states:
for phrase,introduced in self.candidates(start,end,state):
total = state.total+phrase.evaluation.total*(end-start)
successors.append(State((*state.phrases,phrase),state.home or (introduced if start>=0 and len(introduced)>=3 else ()),total))
if not successors:
raise SearchExhausted(f'No valid complete phrase at {start}–{end}; failures: {dict(self.failures)}')
if start>=0 and not openings:
openings = [s.phrases[-1] for s in successors]
successors.sort(key=self.rank)
states = tuple(generator.solo.sequences.diverse(successors,self.rank,
self.character,self.rules['histories']))
for state in states:
self.coverage[state.phrases[-1].purpose]['retained_histories'] += 1
self.frontiers.append({'start':str(start),'end':str(end),'completed':len(successors),'retained':len(states),'best':str(min(s.total for s in states))})
start = end
self.completed = tuple(sorted(states,key=self.rank))
winner = self.completed[0]
selected_opening = next((p for p in winner.phrases if p.start>=0),None)
if selected_opening is not None:
remaining = [p for p in openings if p.notes!=selected_opening.notes]
examples = (selected_opening,*generator.solo.sequences.diverse(remaining,
lambda p:(p.evaluation.total,p.id),lambda p:self.character(State((p,))),3))
self.opening_candidates = [{'selected_in_final_solo':p is selected_opening,'start':str(p.start),'end':str(p.end),
'purpose':p.purpose,'ending':p.ending,'evaluation':dataclasses.asdict(p.evaluation),
'events':[(str(n.start),str(n.end),n.pitch.number()) for n in p.notes]} for p in examples]
self.timings['phrase_search_seconds'] = time.monotonic()-began
self.winner_evaluation = generator.solo.selection.evaluate(winner.notes,self.start,self.end,self.harmony,
home=winner.home,contracts=tuple(c for p in winner.phrases for c in p.expressions),center=self.phrases.center)
for k,v in self.phrases.proposals.items():
self.coverage[k]['proposed'] = v
for p in winner.phrases:
self.coverage[p.purpose]['selected'] += 1
self.decisions = [{'start':str(p.start),'end':str(p.end),'purpose':p.purpose,'ending':p.ending,'evaluation':dataclasses.asdict(p.evaluation)} for p in winner.phrases]
return winner.phrases