"""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=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