Solo
ci / checks (push) Failing after 23m43s

This commit is contained in:
khodowany
2026-10-02 06:33:01 -05:00
parent a0bd19b3ba
commit 5fff5a165e
13 changed files with 786 additions and 211 deletions
+182 -107
View File
@@ -10,7 +10,7 @@ from fractions import Fraction as F
import generator.model
import generator.harmony
from generator.solo import types, policy, sequences, routes, references, similarity
from generator.solo import types, policy, sequences, routes, references, similarity, phrasing
def source_notes(analysis):
@@ -18,34 +18,6 @@ def source_notes(analysis):
event_id=n.sources[0],position=None) for n in analysis.melody if n.end>analysis.body_offset)
def windows(analysis, measures, end, harmony):
"""Existing phrase representation; source extraction seams do not close it."""
bounds=[F()]
for measure in measures:bounds.append(bounds[-1]+generator.model.fraction(measure['duration']))
melody=source_notes(analysis); result=[]; index=0
while bounds[index]<end:
stop=min(index+4,len(measures))
# Permit an extension through a cadence rather than closing on its dominant.
last=harmony.at(min(end,bounds[stop])-F(1,100))
if last and last.chord!='N.C.' and generator.harmony.degrees(generator.harmony.parse_symbol(last.chord)).get(7)==-1 and generator.harmony.degrees(generator.harmony.parse_symbol(last.chord)).get(3)==0:
for size in (6,8):
candidate=min(index+size,len(measures)); span=harmony.at(min(end,bounds[candidate])-F(1,100))
if span and span.chord!='N.C.' and generator.harmony.degrees(generator.harmony.parse_symbol(span.chord)).get(3)==-1:
stop=candidate;break
start,finish=bounds[index],min(end,bounds[stop])
notes=tuple(dataclasses.replace(n,start=max(start,n.start),end=min(finish,n.end),continuation=n.start<start)
for n in melody if n.start<finish and n.end>start)
result.append(types.SourcePhrase(f'theme_window_{start}_{finish}',start,finish,notes,(),''))
index=stop
# Explicit missing harmony interrupts sounding material, not thematic memory.
divided=[]
for window in result:
cuts=sorted({window.start,window.end,*(max(window.start,s.start) for s in harmony.spans if not s.scale and s.start<window.end and s.end>window.start),*(min(window.end,s.end) for s in harmony.spans if not s.scale and s.start<window.end and s.end>window.start)})
for start,finish in zip(cuts,cuts[1:]):
notes=tuple(dataclasses.replace(n,start=max(start,n.start),end=min(finish,n.end),continuation=n.start<start) for n in window.notes if n.start<finish and n.end>start)
divided.append(dataclasses.replace(window,id=f'theme_window_{start}_{finish}',start=start,end=finish,notes=notes))
return tuple(divided)
def silence(source,incoming,part,harmony,tempo,seed):
from generator.solo import realization
@@ -55,7 +27,8 @@ def silence(source,incoming,part,harmony,tempo,seed):
draft=types.MelodicDraft(plan.id,source.start,source.end,(),(),(),(),(),incoming.home,'space',source_phrase=source,source_reference=incoming.source_reference)
phrase=realization.checked_phrase(plan,draft,(),incoming.treatments,incoming,harmony,tempo)
if phrase is None:return ()
state=dataclasses.replace(phrase.state,home=incoming.home,source_reference=incoming.source_reference,callbacks=incoming.callbacks)
state=dataclasses.replace(phrase.state,home=incoming.home,source_reference=incoming.source_reference,callbacks=incoming.callbacks,
history=incoming.history,returns=incoming.returns,closures=incoming.closures,register_peak=incoming.register_peak)
original=dataclasses.replace(draft,notes=source.notes)
return (dataclasses.replace(phrase,state=state,initial=original,refined=draft,score=0.),)
@@ -76,7 +49,8 @@ def short_fragment(source,incoming,part,harmony,tempo,seed,final):
plan=types.PhrasePlan(draft.id,source.start,source.end,motif,'fragment','thematic_recomposition',None,(41,76),F(5,4),F(),ending='sustain' if final else 'connected',closure='closed' if final else 'open',passage_id=source.id,passage_start=source.start,passage_end=source.end)
phrase=realization.checked_phrase(plan,draft,(note,),incoming.treatments,incoming,harmony,tempo)
if phrase:
state=dataclasses.replace(phrase.state,home=incoming.home,source_reference=incoming.source_reference,callbacks=incoming.callbacks)
state=dataclasses.replace(phrase.state,home=incoming.home,source_reference=incoming.source_reference,callbacks=incoming.callbacks,
history=incoming.history,returns=incoming.returns,closures=incoming.closures,register_peak=incoming.register_peak)
result.append(dataclasses.replace(phrase,state=state,initial=draft,refined=draft))
return tuple(result)
@@ -107,7 +81,9 @@ def feature(reference, actual, *, treatment=None):
required={'rhythmic_seed':('rhythm',),'contour_seed':('contour',),
'opening_fragment':('rhythm','pitches'),'characteristic_interval':('exact_intervals',),
'repeat_with_harmonic_adaptation':('rhythm','contour'),'answer':('contour',),
'extend':('rhythm','contour'),'fragment':('rhythm',),'callback':('rhythm','pitches')}[treatment]
'extend':('contour',),'fragment':('rhythm',),'callback':('rhythm','pitches'),
'return':('rhythm','intervals'), 'develop_rhythm':('rhythm',),
'develop_contour':('intervals',), 'continue':('contour',)}[treatment]
original=reference.notes
origin=original[0].start
octave=12*round((actual[0].pitch.number()-original[0].pitch.number())/12)
@@ -122,6 +98,17 @@ def rhythm_windows(library,meter,length,context):
key=('theme-rhythm',id(library),meter,length)
if key not in context.pools:
patterns={}
bar=F(meter.split('/')[0])*4/int(meter.split('/')[1])
if length>bar:
# The two-bar catalog contains quarter-note masks. Include combinations
# of the richer one-bar material as complete timing alternatives, rather
# than accidentally excluding eighths/dotted values by snippet length.
# Pitch paths and feature contracts span the assembled rhythm's seams.
split=bar
for left,left_id in rhythm_windows(library,meter,split,context):
for right,right_id in rhythm_windows(library,meter,length-split,context):
combined=(*left,*((a+split,b+split) for a,b in right))
if len(combined)<=16:patterns.setdefault(combined,left_id+' + '+right_id)
for identifier,pattern,total in drafts.catalog(library,meter):
if total<length:continue
for offset in sorted({F(),*(a for a,_ in pattern)}):
@@ -134,13 +121,14 @@ def rhythm_windows(library,meter,length,context):
return context.pools[key]
def patterns(ref, start, finish, rhythms, meter, context, *, home=None, operation='introduce', terminal=False):
def patterns(ref, start, finish, rhythms, meter, context, *, home=None, operation='introduce', terminal=False, ending=None):
length=finish-start; values={}
if home and operation=='repeat_with_harmonic_adaptation':
if home and operation in ('repeat_with_harmonic_adaptation','return'):
pattern=tuple((n.start,n.end) for n in home.notes if n.end<=length)
if pattern:values[pattern]='heard-theme'
prefix=tuple((n.start,n.end) for n in ref.notes)
if ref.treatment in ('rhythmic_seed','opening_fragment','callback') or operation in ('extend','fragment','repeat_with_harmonic_adaptation'):
fixed_rhythm=ref.treatment in ('rhythmic_seed','opening_fragment','callback') or operation in ('fragment','repeat_with_harmonic_adaptation','return','develop_rhythm')
if fixed_rhythm:
duration=prefix[-1][1]
if duration==length:values[prefix]=ref.id
if duration<length:
@@ -150,15 +138,28 @@ def patterns(ref, start, finish, rhythms, meter, context, *, home=None, operatio
values.update(rhythm_windows(rhythms,meter,length,context))
if home and operation=='answer':
for p,label in rhythm_windows(rhythms,meter,length,context):values.setdefault(p,label)
if terminal:
if not fixed_rhythm:
# Develop the heard timing, preserving total duration. Eighth/quarter/dotted
# alternatives are actual contracts, not failed copies of a rhythmic seed.
for original,label in tuple(values.items()):
for hold in (F(1,2),F(1),F(3,2),F(2)):
for release in (F(),F(1,2)):
arrival=length-hold-release
head=tuple((a,min(b,arrival)) for a,b in original if a<arrival)
if len(head)>=len(ref.notes):values[(*head,(arrival,length-release))]=label+f'; arrival {hold}, release {release}'
return sequences.diverse(tuple(values.items()),lambda x:(max(0,len(x[0])-1.5*float(length),float(length)-len(x[0])),policy.stable_rank(ref.id,x[0])),
lambda x:(len(x[0]),x[0][-1][1]-x[0][-1][0],x[0][0][0]),4)
for i,(a,b) in enumerate(original):
if b-a==1:
values[(*original[:i],(a,a+F(1,2)),(a+F(1,2),b),*original[i+1:])]=label+'; diminution'
for i,((a,b),(c,d)) in enumerate(zip(original,original[1:])):
if b!=c:continue
for split in (a+F(1,2),a+F(3,2),d-F(1,2)):
if a<split<d:
values[(*original[:i],(a,split),(split,d),*original[i+2:])]=label+'; rhythmic alteration'
if ending:
arrivals={}
arrival=ending.earliest-start
for original,label in values.items():
head=tuple((a,min(b,arrival)) for a,b in original if a<arrival)
if len(head)>=len(ref.notes):
arrivals[(*head,(arrival,ending.hold_until-start))]=label+'; planned sustained close'
values=arrivals
return sequences.diverse(tuple(values.items()),lambda x:(len(x[0])>16,policy.stable_rank(ref.id,x[0])),
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):
@@ -196,6 +197,14 @@ def instantiate(templates,first,steps,harmony):
def prefix_paths(templates,ref,first,harmony,library):
if not hasattr(harmony,'theme_prefixes'):harmony.theme_prefixes={}
key=(templates[:len(ref.notes)],ref,first,id(library))
if key not in harmony.theme_prefixes:
harmony.theme_prefixes[key]=_prefix_paths(templates,ref,first,harmony,library)
return harmony.theme_prefixes[key]
def _prefix_paths(templates,ref,first,harmony,library):
count=len(ref.notes); original=ref.notes
if count==1:return ((dataclasses.replace(templates[0],pitch=first),),)
if ref.treatment in ('opening_fragment','characteristic_interval','callback'):
@@ -232,64 +241,91 @@ class Unit:
routes: tuple[types.RouteCandidate,...] = ()
def unit_candidates(ref,start,finish,incoming,harmony,library,rhythms,meter,context,*,home=None,operation='introduce',terminal=False,batch=0):
# Position and the recent realization participate even when two paths share a last pitch.
def unit_candidates(ref,start,finish,incoming,harmony,library,rhythms,meter,context,*,home=None,operation='introduce',terminal=False,batch=0,ending=None,goal=None):
# 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,terminal,batch,harmony.fingerprint,id(library),id(rhythms),policy.DEFAULT)
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)
if key in context.results:
context.diagnostics['cache_hits']+=1;return context.results[key]
result=[]; count=len(ref.notes)
for pattern,label in patterns(ref,start,finish,rhythms,meter,context,home=home,operation=operation,terminal=terminal):
if len(pattern)<count:continue
jobs=[]
for pattern,label in patterns(ref,start,finish,rhythms,meter,context,home=home,operation=operation,terminal=terminal,ending=ending):
if len(pattern)<count or len(pattern)>16:continue
templates=tuple(types.Note(start+a,start+b,ref.notes[0].pitch,tuple(s for n in ref.notes for s in n.sources),
'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)
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
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()
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)
job_limit=policy.DEFAULT.theme_route_jobs*(batch+1)
for job in jobs:
if len(selected)>=job_limit:break
if job not in selected:selected.append(job)
for _,templates,label,first,target in selected[:job_limit]:
proposed=[]
if home and operation=='repeat_with_harmonic_adaptation' and len(templates)==len(home.notes):
if home and operation in ('repeat_with_harmonic_adaptation','return') and len(templates)==len(home.notes):
heard=instantiate(templates,first,home.degree_steps,harmony)
if heard:proposed.append((heard,()))
for prefix in prefix_paths(templates,ref,first,harmony,library):
if heard and heard[-1].pitch==target:proposed.append((heard,()))
for prefix in prefix_paths(templates,ref,first,harmony,library)[:2+batch]:
if len(prefix)==len(templates):
proposed.append((prefix,()));continue
base=prefix[-1].pitch.number()
for target in pitches(harmony,templates[-1],base,members=True,limit=4 if batch else 2):
tail=(prefix[-1],*templates[count:])
tail=(*tail[:-1],dataclasses.replace(tail[-1],pitch=target))
for route in routes.candidates(tail,{tail[-1].event_id},harmony,library,(41,76),operation+' '+ref.id):
proposed.append(((*prefix[:-1],*route.notes),(route,)))
if prefix[-1].pitch==target:proposed.append((prefix,()))
continue
tail=(prefix[-1],*templates[count:])
tail=(*tail[:-1],dataclasses.replace(tail[-1],pitch=target))
for route in routes.candidates(tail,{tail[-1].event_id},harmony,library,(41,76),operation+' '+ref.id)[:3+batch]:
proposed.append(((*prefix[:-1],*route.notes),(route,)))
seen=set()
for actual,connections in proposed:
identity=tuple(n.pitch.number() for n in actual)
if identity in seen:continue
seen.add(identity)
core=actual[:count]; contract=feature(ref,core,treatment=operation if operation!='introduce' else None)
if references.feature_error(contract,actual):continue
last=actual[-1]
if last.pitch.number()%12 not in harmony.members(last.start,last.end):continue
checked=harmony.evaluate(actual,until=finish,complete=True)
if not checked.valid:continue
density=len(actual)*4/float(finish-start)
entry=max(0,abs(first.number()-incoming.last.pitch.number())-5)/12 if incoming.last else 0.
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)
# A repeat preserves the full heard idea. Answers and extensions
# preserve their declared fragment, not every event of the old ending.
development=references.relationship(actual,home) if home and operation=='repeat_with_harmonic_adaptation' else 0.
score=2*checked.exposure+entry+leaps+development+.35*max(0,4-density,density-6)
anchor=types.Anchor(last.event_id+':arrival',last.start,last.start,last.pitch,harmony.members(last.start,last.end),(41,76),
last.end-last.start,'terminal' if terminal else 'intermediate',harmony.at(last.start).chord,hold_until=last.end)
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
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))
unique={tuple((n.start,n.end,n.pitch) for n in u.notes):u for u in result}
selected=sequences.diverse(tuple(unique.values()),lambda u:(u.score,policy.stable_rank(ref.id,u.notes)),
lambda u:(u.notes[0].pitch.number()//12,u.notes[-1].pitch.number()%12,tuple(n.end-n.start for n in u.notes[:count])),8)
lambda u:(u.notes[0].pitch.number()//12,u.notes[-1].pitch.number(),tuple(n.end-n.start for n in u.notes[:count])),policy.DEFAULT.theme_unit_alternatives)
context.diagnostics['generated_candidates']+=len(selected)
context.results[key]=selected
return selected
def home_reference(home,operation):
notes=home.notes[:min(4,len(home.notes))]
# A short characteristic beginning leaves room for a newly composed answer.
while len(notes)>3 and notes[-1].end>home.duration/2:notes=notes[:-1]
return types.SourceReference(home.id,home.parent,home.source_start,home.source_start+home.duration,notes,
'rhythmic_seed' if operation in ('repeat_with_harmonic_adaptation','extend','fragment') else 'contour_seed')
# Prefer a distinguishing turn/leap wherever it actually occurred. The heard
# home remains immutable; a development never becomes the new source theme.
groups=[home.notes[i:i+3] for i in range(max(1,len(home.notes)-2))]
def rank(group):
intervals=[b.pitch.number()-a.pitch.number() for a,b in zip(group,group[1:])]
turn=len(intervals)>1 and intervals[0]*intervals[1]<0
return (-int(turn),-max((abs(x) for x in intervals),default=0),group[0].start)
notes=min(groups,key=rank)
origin=notes[0].start
notes=tuple(dataclasses.replace(n,start=n.start-origin,end=n.end-origin) for n in notes)
treatment='rhythmic_seed' if operation in ('repeat_with_harmonic_adaptation','fragment','return','develop_rhythm') else 'contour_seed'
return types.SourceReference(home.id,home.parent,home.source_start,home.source_start+home.duration,notes,treatment)
def make_home(unit,harmony,start=None,end=None):
@@ -300,96 +336,135 @@ def make_home(unit,harmony,start=None,end=None):
return dataclasses.replace(home,degree_steps=relative_steps(unit.notes,harmony))
def phrase_candidates(source,analysis,incoming,part,harmony,library,rhythms,meter,tempo,context,seed,*,stage='elaborated',batch=0,final=False):
def phrase_candidates(source,analysis,incoming,part,harmony,library,rhythms,meter,tempo,context,seed,*,stage='elaborated',batch=0,final=False,solo_end=None):
from generator.solo import leads,realization
if source.end-source.start<2:
return short_fragment(source,incoming,part,harmony,tempo,seed,final)
bar=F(meter.split('/')[0])*4/int(meter.split('/')[1])
unit_length=2*bar
solo_end=solo_end or max(c.span.end for c in harmony.choices)
head=source_notes(analysis)
seed_key=('source-seeds',analysis,bar,seed)
if seed_key not in context.pools:context.pools[seed_key]=seeds(analysis,bar,seed)
roots=context.pools[seed_key]
states=[(0.,(),incoming.home,incoming)]
ending_options=phrasing.endings(source,incoming,part,harmony,bar,limit=8*(batch+1))
states=[(0.,(),incoming.home,incoming,ending) for ending in ending_options]
cursor=source.start
while cursor<source.end:
while cursor<source.end and states:
finish=min(cursor+unit_length,source.end)
if source.end-finish<6 or any(ending.earliest<finish for *_,ending in states):finish=source.end
next_states=[]
for cost,units,home,state in states:
for cost,units,home,state,ending in states:
introducing=home is None
if introducing:
requests=[(r,'introduce') for r in roots]
else:
operations=('repeat_with_harmonic_adaptation',) if cursor==source.start and source.start<=4*bar else ('repeat_with_harmonic_adaptation','answer','extend')
operations=['continue'] if finish==source.end and cursor>source.start else ['develop_contour','develop_rhythm']
if cursor==source.start:operations.append('return')
requests=[(home_reference(home,op),op) for op in operations]
used=incoming.callbacks+sum(u.operation=='callback' for u in units)
if cursor>=8*bar and cursor==source.start and used<2:
requests.extend((dataclasses.replace(r,treatment='callback'),'callback') for r in roots[:2] if r.end-r.start<=bar)
# A uniform duration alone is not identifying musical evidence.
requests=[(r,op) for r,op in requests if op!='develop_rhythm' or len({n.end-n.start for n in r.notes})>1 or any(a.end!=b.start for a,b in zip(r.notes,r.notes[1:]))]
if cursor>=8*bar and cursor==source.start and incoming.callbacks<2:
requests.extend((dataclasses.replace(r,treatment='callback'),'callback') for r in roots[:1] if r.end-r.start<=bar)
for ref,operation in requests:
if introducing and source.end-source.start<2*bar and ending.earliest-cursor<ref.notes[-1].end:
# A missing-harmony boundary can leave less than one full statement.
# Declare a smaller contour seed; never pretend it proves the full motif.
count=min(3,int(2*(ending.earliest-cursor)),len(ref.notes))
if count<2:
if len(ref.notes)!=1:continue
n=dataclasses.replace(ref.notes[0],end=min(ref.notes[0].end,F(1),ending.earliest-cursor))
ref=dataclasses.replace(ref,id=ref.id+':constrained_pitch',notes=(n,),treatment='opening_fragment')
else:
ref=dataclasses.replace(ref,id=ref.id+':constrained_contour',notes=ref.notes[:count],treatment='contour_seed')
if operation=='continue' and state.notes:
recent=state.notes[-min(3,len(state.notes)):]
origin=recent[0].start
ref=dataclasses.replace(ref,notes=tuple(dataclasses.replace(n,start=n.start-origin,end=n.end-origin) for n in recent),treatment='contour_seed')
if ref.notes[-1].end>finish-cursor:
reduced=tuple(n for n in ref.notes if n.end<=finish-cursor)
if len(reduced)<2:continue
ref=dataclasses.replace(ref,notes=reduced,treatment='contour_seed')
operation='answer'
operation='continue'
terminal=finish==source.end
base=state.last.pitch.number() if state.last else ref.notes[0].pitch.number()
goal=ending.pitch.number() if terminal else round(base+(ending.pitch.number()-base)*float((finish-cursor)/(source.end-cursor)))
options=unit_candidates(ref,cursor,finish,state,harmony,library,rhythms,meter,context,home=home,operation=operation,
terminal=final and finish==source.end,batch=batch)
terminal=terminal,batch=batch,ending=ending if terminal else None,goal=goal)
for unit in options:
if introducing and similarity.near_copy(unit.notes,head,cursor,finish)['near_copy']:
context.diagnostics['near_copy_rejections']=context.diagnostics.get('near_copy_rejections',0)+1;continue
actual_home=home or make_home(unit,harmony,cursor,finish)
following=dataclasses.replace(state,home=actual_home,last=unit.notes[-1],notes=unit.notes,source_reference=state.source_reference or ref)
next_states.append((cost+unit.score,(*units,unit),actual_home,following))
states=sequences.diverse(next_states,lambda v:(v[0],policy.stable_rank(seed,tuple(u.notes for u in v[1]))),
lambda v:(v[2].id,v[1][-1].notes[-1].pitch.number()),2 if incoming.home and finish<source.end else 8)
figure=phrasing.heard(unit.notes,operation,unit.anchor)
repetition=phrasing.repetition(figure,state.history,bar)
appearance=phrasing.appearance_cost(operation,state,cursor,solo_end) if home else 0.
history=phrasing.remember(state.history,figure,bar)
following=dataclasses.replace(state,home=actual_home,last=unit.notes[-1],notes=unit.notes,source_reference=state.source_reference or ref,
history=history,returns=(*state.returns,*((cursor,) if operation=='return' else ())),
register_peak=max(state.register_peak,*(n.pitch.number() for n in unit.notes)))
next_states.append((cost+unit.score+.8*repetition+appearance,(*units,unit),actual_home,following,ending))
def rank(value):
cost,units,home,state,ending=value
attacks=sum(len(u.notes) for u in units)
remaining=max(F(),ending.earliest-finish)
possible=attacks+2*float(remaining)+(0 if finish==source.end else 1)
expected=float(source.end-source.start)
activity_bound=.35*max(0,expected-possible,attacks-1.5*expected)/max(1,float(source.end-source.start)/4)
return cost/max(1,len(units))+activity_bound,policy.stable_rank(seed,tuple(u.notes for u in units))
states=sequences.diverse(next_states,rank,
lambda v:(v[2].id,v[4].pitch.number(),v[4].duration,len(v[1][-1].notes),v[1][-1].operation),policy.DEFAULT.theme_continuations)
if not states:return ()
cursor=finish
result=[]
for cost,units,home,state in states:
for cost,units,home,state,ending in states:
plain=tuple(n for u in units for n in u.notes)
if similarity.near_copy(plain,head,source.start,source.end)['near_copy']:
context.diagnostics['near_copy_rejections']=context.diagnostics.get('near_copy_rejections',0)+1;continue
features=tuple(u.feature for u in units)
anchors=tuple(u.anchor for u in units)
# Cell tails are not compulsory melodic arrivals. Only the selected close
# has a sounding contract; intermediate turns remain editable.
anchors=(ending,)
tails=(types.SustainedTail(ending.earliest,ending.hold_until,ending.pitch.number()),)
options=[(plain,(),())]
if stage=='elaborated':
protected=(*features,types.IdentifyingFeature('destinations',tuple(n.event_id for n in plain if any(n.event_id+':arrival'==a.id for a in anchors)),(),(),'',()))
for actual,edit,route in leads.elaborate(plain,protected,harmony,library,rhythms,meter,source,sustain_entry=True):
for actual,edit,route in leads.elaborate(plain,features,harmony,library,rhythms,meter,source,sustain_entry=True,tails=tails):
options.append((actual,(edit,),(route,)))
for notes,edits,selected_routes in options:
identifier=part+'_'+source.id+'_'+policy.stable_rank(seed,leads.signature(notes))[:12]
transforms=tuple(types.Transformation(identifier+f':{i}',u.reference.id,home.id,u.operation,u.notes[0].start,u.notes[-1].end,
tuple(n.event_id for n in notes if u.notes[0].start<=n.start<u.notes[-1].end),tuple((n.start-u.notes[0].start,n.end-u.notes[0].start) for n in notes if u.notes[0].start<=n.start<u.notes[-1].end),preserves=(),source=u.reference.id+'; rhythm '+u.source) for i,u in enumerate(units))
source_ref=state.source_reference
draft=types.MelodicDraft(identifier,source.start,source.end,notes,transforms,
tuple(a.id.removesuffix(':arrival') for a in anchors),(),anchors,home,'introduce' if not incoming.home else 'develop',
draft=types.MelodicDraft(identifier,source.start,source.end,notes,transforms,(),(),anchors,home,'introduce' if not incoming.home else 'develop',
features=features,reference=source_ref.id,reference_motif=home,source_phrase=source,source_reference=source_ref,
source_links=tuple((n.event_id,n.sources) for n in notes))
# One shared musical context crosses computational/source boundaries.
activity=[];cursor=source.start
for n in notes:
if n.start>cursor:activity.append(types.ActivitySpan(cursor,n.start,'space',F(),F()))
activity.append(types.ActivitySpan(n.start,n.end,'active',F(5,4),F()))
cursor=n.end
if cursor<source.end:activity.append(types.ActivitySpan(cursor,source.end,'space',F(),F()))
source_links=tuple((n.event_id,n.sources) for n in notes),sustained_tails=tails)
motif=types.Motif(home.id,home.notes,home.duration,(), 'realized solo theme from '+source_ref.id)
landmark=types.Landmark(ending.earliest,ending.latest,ending.pitch.number(),ending.pitch.number(),0,'sustained destination')
plan=types.PhrasePlan(identifier,source.start,source.end,motif,draft.relationship,'thematic_recomposition',None,(41,76),F(5,4),
source.end-notes[-1].end,tuple(activity),(), 'sustain' if final else 'connected','closed' if final else 'open',seed,
source.end-ending.hold_until,phrasing.activity(source,ending),(landmark,), 'sustain','closed',seed,
source.id,source.start,source.end,(anchors,))
phrase=realization.checked_phrase(plan,draft,notes,incoming.treatments,incoming,harmony,tempo,edits)
if phrase is None:continue
density=len(notes)*4/float(source.end-source.start)
score=cost/len(units)+.35*(max(0,4-density,density-6)-max(0,4-len(plain)*4/float(source.end-source.start),len(plain)*4/float(source.end-source.start)-6))
score=phrasing.evaluate(notes,incoming,harmony,source.start,source.end,bar)
appearance_state=incoming
for unit in units:
score+=phrasing.appearance_cost(unit.operation,appearance_state,unit.notes[0].start,solo_end)/len(units)
if unit.operation=='return':
appearance_state=dataclasses.replace(appearance_state,returns=(*appearance_state.returns,unit.notes[0].start))
score+=.05*phrase.state.travel/max(1,len(notes))/2000
current=dataclasses.replace(phrase.state,home=home,source_reference=source_ref,callbacks=incoming.callbacks+sum(u.operation=='callback' for u in units))
# Store the actual introduced sound, including any legal elaboration.
history=incoming.history
for transform in transforms:
group=tuple(n for n in notes if n.event_id in transform.events)
if group:history=phrasing.remember(history,phrasing.heard(group,transform.operation,ending if group[-1].end==ending.hold_until else None),bar)
current=dataclasses.replace(phrase.state,home=home,source_reference=source_ref,callbacks=incoming.callbacks+sum(u.operation=='callback' for u in units),
history=history,returns=state.returns,closures=incoming.closures+1,register_peak=max(state.register_peak,*(n.pitch.number() for n in notes)))
if not incoming.home:
introduced=tuple(n for n in notes if n.start<units[0].notes[-1].end)
heard=dataclasses.replace(home,notes=tuple(dataclasses.replace(n,start=n.start-source.start,end=n.end-source.start,position=None) for n in introduced),
degree_steps=relative_steps(introduced,harmony))
current=dataclasses.replace(current,home=heard,themes=(heard,))
source_control=leads.shift(source.notes, next((o for o in (0,1,-1,2,-2) if all(41<=n.pitch.number()+12*o<=76 for n in source.notes)),0))
original=dataclasses.replace(draft,id=identifier+':source',notes=source_control,features=(),anchors=(),structural=())
source_control=leads.shift(source.notes,next((o for o in (0,1,-1,2,-2) if all(41<=n.pitch.number()+12*o<=76 for n in source.notes)),0))
original=dataclasses.replace(draft,id=identifier+':source',notes=source_control,features=(),anchors=(),structural=(),sustained_tails=())
refined=dataclasses.replace(draft,id=identifier+':plain',notes=plain)
result.append(dataclasses.replace(phrase,score=score,state=current,initial=original,refined=refined,routes=(*tuple(r for u in units for r in u.routes if all(any((n.event_id,n.start,n.end,n.pitch)==(a.event_id,a.start,a.end,a.pitch) for a in notes) for n in r.notes)),*selected_routes)))
return sequences.diverse(result,lambda p:(p.score,policy.stable_rank(seed,p.draft.id)),
lambda p:(p.state.home.id,p.state.last.pitch.number(),sum(e.kind=='complete_route' for e in p.edits)),8)
lambda p:(p.state.home.id,p.state.last.pitch.number(),p.anchors[-1].duration,len(p.state.returns)),policy.DEFAULT.phrase_alternatives)