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