84 lines
3.7 KiB
Python
84 lines
3.7 KiB
Python
"""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
|
||
|
||
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))
|
||
|
||
def compose(self):
|
||
began = time.monotonic(); self.frontiers = []; self.decisions = []
|
||
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)}')
|
||
successors.sort(key=self.rank)
|
||
states = tuple(generator.solo.sequences.diverse(successors,self.rank,
|
||
lambda s:(s.phrases[-1].purpose,s.phrases[-1].direction,s.phrases[-1].ending),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]
|
||
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
|