Simplification

This commit is contained in:
khodowany
2026-10-05 17:24:16 -05:00
parent bcaf2f1403
commit db7966a430
12 changed files with 639 additions and 123 deletions
+22 -1
View File
@@ -33,6 +33,7 @@ class Builder:
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
@@ -51,8 +52,18 @@ class Builder:
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)
@@ -63,15 +74,25 @@ class Builder:
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,
lambda s:(s.phrases[-1].purpose,s.phrases[-1].direction,s.phrases[-1].ending),self.rules['histories']))
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)