This commit is contained in:
khodowany
2026-10-03 10:12:12 -05:00
parent 5fff5a165e
commit 59afd78205
14 changed files with 666 additions and 41 deletions
+16 -7
View File
@@ -162,9 +162,12 @@ def patterns(ref, start, finish, rhythms, meter, context, *, home=None, operatio
lambda x:(len(x[0]),tuple(b-a for a,b in x[0][:3]),x[0][0][0]),policy.DEFAULT.theme_rhythm_alternatives)
def pitches(harmony,note,base,*,members=False,limit=6):
def pitches(harmony,note,base,*,members=False,limit=6,incoming=None):
from generator.solo.leads import available
domain=available(harmony,note.start,note.end,members=members or note.end-note.start>=2)
if members:
return phrasing.destination_options(domain,lambda p:(phrasing.destination_cost(p,note.start,note.end,harmony,incoming,base),p.number()),
lambda p:p,lambda p:note.start,harmony,limit)
return sequences.diverse(domain,lambda p:(abs(p.number()-base),p.number()),
lambda p:((p.number()-base>0)-(p.number()-base<0),p.number()//12),limit)
@@ -245,7 +248,7 @@ def unit_candidates(ref,start,finish,incoming,harmony,library,rhythms,meter,cont
# History is evaluated by the caller after retrieving these concrete options.
# Every state component used to construct this pool is in its cache key.
if not hasattr(harmony,'fingerprint'):harmony.fingerprint=policy.stable_rank('harmony',harmony.choices)
key=('theme-unit',ref,start,finish,incoming.last,incoming.direction,incoming.treatments,home,operation,ending,goal,batch,harmony.fingerprint,id(library),id(rhythms),policy.DEFAULT)
key=('theme-unit',ref,start,finish,incoming.last,incoming.direction,incoming.treatments,incoming.history,home,operation,ending,goal,batch,harmony.fingerprint,id(library),id(rhythms),policy.DEFAULT)
if key in context.results:
context.diagnostics['cache_hits']+=1;return context.results[key]
result=[]; count=len(ref.notes)
@@ -256,19 +259,24 @@ def unit_candidates(ref,start,finish,incoming,harmony,library,rhythms,meter,cont
'theme',event_id=f'{ref.id}:{start}:{i}') for i,(a,b) in enumerate(pattern))
base=incoming.last.pitch.number() if incoming.last else ref.notes[0].pitch.number()
firsts=pitches(harmony,templates[0],base,limit=6 if batch else 3)
targets=(ending.pitch,) if ending else pitches(harmony,templates[-1],goal if goal is not None else base,members=True,limit=policy.DEFAULT.theme_destinations)
targets=(ending.pitch,) if ending else pitches(harmony,templates[-1],goal if goal is not None else base,members=True,limit=policy.DEFAULT.theme_destinations,incoming=incoming)
for first in firsts:
for target in targets:
# Rank joint jobs, retaining rhythm and destination coverage before
# expensive path instantiation. The budget is spent on complete routes.
rank=max(0,abs(first.number()-base)-5)/12+abs(target.number()-(goal if goal is not None else base))/48
rank=max(0,abs(first.number()-base)-5)/12+phrasing.destination_cost(target,templates[-1].start,templates[-1].end,harmony,incoming,goal if goal is not None else base,terminal)
jobs.append((rank,templates,label,first,target))
jobs.sort(key=lambda j:(j[0],policy.stable_rank(ref.id,(j[1],j[3],j[4]))))
selected=[];rhythm_seen=set();target_seen=set();entry_seen=set()
selected=[];rhythm_seen=set();target_seen=set();entry_seen=set();degrees_seen=set()
for job in jobs:
family=phrasing.destination_family(job[4],job[1][-1].start,harmony)
if family not in degrees_seen:
selected.append(job);degrees_seen.add(family)
for job in jobs:
rhythm=tuple((n.start,n.end) for n in job[1]);target=job[4].number();entry=job[3].number()
if rhythm not in rhythm_seen or target not in target_seen or entry not in entry_seen:
selected.append(job);rhythm_seen.add(rhythm);target_seen.add(target);entry_seen.add(entry)
if job not in selected:selected.append(job)
rhythm_seen.add(rhythm);target_seen.add(target);entry_seen.add(entry)
job_limit=policy.DEFAULT.theme_route_jobs*(batch+1)
for job in jobs:
if len(selected)>=job_limit:break
@@ -301,7 +309,7 @@ def unit_candidates(ref,start,finish,incoming,harmony,library,rhythms,meter,cont
leaps=sum(max(0,abs(b.pitch.number()-a.pitch.number())-5)/12 for a,b in zip(actual,actual[1:]))/max(1,len(actual)-1)
direction=min(1,abs(last.pitch.number()-goal)/12) if goal is not None else 0.
# Activity belongs to the complete phrase; contracts are pass/fail.
score=2*checked.exposure+entry+leaps+.35*direction
score=2*checked.exposure+entry+leaps+phrasing.destination_cost(last.pitch,last.start,last.end,harmony,incoming,goal,terminal)
anchor=ending or types.Anchor(last.event_id+':arrival',last.start,last.start,last.pitch,harmony.members(last.start,last.end),(41,76),
last.end-last.start,'intermediate',harmony.at(last.start).chord,hold_until=last.end)
result.append(Unit(actual,contract,ref,operation,anchor,score,label,connections))
@@ -445,6 +453,7 @@ def phrase_candidates(source,analysis,incoming,part,harmony,library,rhythms,mete
phrase=realization.checked_phrase(plan,draft,notes,incoming.treatments,incoming,harmony,tempo,edits)
if phrase is None:continue
score=phrasing.evaluate(notes,incoming,harmony,source.start,source.end,bar)
score+=sum(phrasing.destination_cost(u.anchor.pitch,u.anchor.earliest,u.anchor.hold_until,harmony,incoming,phrasing.register_goal(incoming,part,u.notes[0].pitch.number()),u.anchor.role=='terminal') for u in units)/len(units)
appearance_state=incoming
for unit in units:
score+=phrasing.appearance_cost(unit.operation,appearance_state,unit.notes[0].start,solo_end)/len(units)