Cleanup
This commit is contained in:
+260
-48
@@ -1,25 +1,42 @@
|
||||
"""Interpretable shared action ranking; no song identity or absolute bar features.
|
||||
"""Canonical sounded-event evaluation, shared by inference and training.
|
||||
|
||||
Complete actions compete by exact cost per quarter, with bounded backtracking.
|
||||
The final report exposes the exact same contributions used by search.
|
||||
Role-specific learned integer coefficients can augment these general priors.
|
||||
Constructor names are not musical evidence. Complete equal-span candidates
|
||||
are compared using this record; local retrieval is explicitly only a heuristic.
|
||||
"""
|
||||
from collections import Counter
|
||||
from fractions import Fraction as F
|
||||
import dataclasses
|
||||
import functools
|
||||
from types import MappingProxyType
|
||||
from generator import vocabulary, harmony as chords, model
|
||||
from generator.solo import expressions, scoring, types
|
||||
|
||||
VERSION='action-selection-1'
|
||||
VERSION='phrase-selection-2'
|
||||
DEFAULT={'excessive_leap':12,'register_distance':1,'oscillation':6,'repeated_attack':3,
|
||||
'unprepared_space':12,'rest_after_rest':24,'source_relation':-2,'theme_relation':-4,
|
||||
'unprepared_space':12,'source_relation':-2,'theme_relation':-4,
|
||||
'sustained_arrival':-1,'directed_motion':-1}
|
||||
|
||||
CONDITIONS={
|
||||
'quality':frozenset(('major','minor','dominant','half_diminished','other')),
|
||||
'degree':frozenset(map(str,range(12))),
|
||||
'motion':frozenset(map(str,range(-12,13))),
|
||||
'metric':frozenset(('barline','beat','eighth','subdivision')),
|
||||
'duration':frozenset(str(F(i,4)).replace('/','_') for i in range(1,17))|{'1_3','2_3'},
|
||||
'source_pair':frozenset(('yes','no')),
|
||||
}
|
||||
|
||||
def rule_conditions(key):
|
||||
if not key.startswith('joint/'):return None
|
||||
pairs=tuple(p.split('=',1) for p in key[6:].split('&'))
|
||||
if not 2<=len(pairs)<=3 or any(len(p)!=2 for p in pairs):raise ValueError('Expected two or three contextual conditions')
|
||||
if len({p[0] for p in pairs})!=len(pairs) or list(pairs)!=sorted(pairs):raise ValueError('Noncanonical contextual rule')
|
||||
if any(k not in CONDITIONS or v not in CONDITIONS[k] for k,v in pairs):raise ValueError('Forbidden contextual rule condition')
|
||||
return dict(pairs)
|
||||
|
||||
|
||||
def validate_weights(weights):
|
||||
durations={str(F(i,4)).replace('/','_') for i in range(1,17)}|{'1_3','2_3'}
|
||||
kinds={'tone','color','color_resolution','anticipation','anticipated_color','approach','enclosure','connection','delayed_approach','space','rest','scalar','arpeggio','return','answer'}
|
||||
allowed={f'kind/{k}' for k in kinds}|{f'duration/{d}' for d in durations}
|
||||
allowed={f'duration/{d}' for d in durations}
|
||||
allowed|={f'harmony/{q}/{p}/{d}' for q in ('major','minor','dominant','half_diminished','other') for p in range(12) for d in durations}
|
||||
allowed|={f'motion/{p}/{d}' for p in range(-12,13) for d in durations}
|
||||
allowed|={f'metric/{m}/{d}' for m in ('barline','beat','eighth','subdivision') for d in durations}
|
||||
@@ -30,9 +47,10 @@ def validate_weights(weights):
|
||||
allowed|={f'continuation/{a}/{b}' for a in ('up','down','repeat') for b in ('up','down','repeat')}
|
||||
allowed|={'source/rhythm_match','theme/realized_identity',*(f'space/{s}' for s in ('unharmonized','breath','after_arrival','delayed_entrance'))}
|
||||
for key,value in weights.items():
|
||||
if key not in allowed or not isinstance(value,int) or isinstance(value,bool):
|
||||
joint=rule_conditions(key)
|
||||
if (key not in allowed and joint is None) or not isinstance(value,int) or isinstance(value,bool) or abs(value)>1024:
|
||||
raise ValueError('Unsupported fitted feature (only shared musical categories): '+key)
|
||||
if any(c not in 'abcdefghijklmnopqrstuvwxyz_0123456789/-' for c in key):
|
||||
if any(c not in 'abcdefghijklmnopqrstuvwxyz_0123456789/-=&' for c in key):
|
||||
raise ValueError('Noncanonical fitted feature: '+key)
|
||||
return dict(weights)
|
||||
|
||||
@@ -43,7 +61,21 @@ def signature(notes):
|
||||
return tuple((n.start-first.start,n.end-n.start,n.pitch.number()-first.pitch.number()) for n in notes)
|
||||
|
||||
|
||||
def matches_signature(notes,expected):
|
||||
"""Exact signature equality, stopping at the first differing feature."""
|
||||
if len(notes)!=len(expected):return False
|
||||
if not notes:return True
|
||||
first=notes[0];pitch=first.pitch.number()
|
||||
return all(n.pitch.number()-pitch==relative and n.end-n.start==duration and n.start-first.start==onset
|
||||
for n,(onset,duration,relative) in zip(notes,expected))
|
||||
|
||||
|
||||
class Selector:
|
||||
@property
|
||||
def weights(self):return self._weights
|
||||
@weights.setter
|
||||
def weights(self,value):self._weights=MappingProxyType(validate_weights(value))
|
||||
|
||||
def __init__(self,harmony,analysis,center,weights=None):
|
||||
self.harmony=harmony;self.analysis=analysis;self.center=center
|
||||
self.weights=validate_weights(weights if weights is not None else vocabulary.role().get('selection_weights',{}))
|
||||
@@ -52,11 +84,29 @@ class Selector:
|
||||
for a,b,c in zip(analysis.melody,analysis.melody[1:],analysis.melody[2:]))
|
||||
self.source_rhythms=Counter(n.end-n.start for n in analysis.melody)
|
||||
self.static={}
|
||||
self.home_signatures={}
|
||||
self.cost_static={};self.cost_weights=None
|
||||
|
||||
def contexts(self,notes,recent=()):
|
||||
history=list(recent[-2:])
|
||||
for n in notes:
|
||||
region=self.harmony.at(n.start)
|
||||
if region and region.chord:
|
||||
chord=expressions.parsed_chord(region.chord)
|
||||
root=int(model.NATURAL[chord['root']['letter']]+model.alteration(chord['root']['alter']))
|
||||
delta=n.pitch.number()-history[-1].pitch.number() if history else None
|
||||
previous=history[-1].pitch.number()-history[-2].pitch.number() if len(history)>1 else None
|
||||
metric='barline' if n.start+self.analysis.body_offset in self.analysis.boundaries else 'beat' if n.start%1==0 else 'eighth' if n.start%1==F(1,2) else 'subdivision'
|
||||
context={'quality':expressions.quality(chord),'degree':str((n.pitch.number()-root)%12),
|
||||
'metric':metric,'duration':str(n.end-n.start).replace('/','_'),
|
||||
'source_pair':'yes' if (previous,delta) in self.source_pairs else 'no'}
|
||||
if delta is not None:context['motion']=str(max(-12,min(12,delta)))
|
||||
yield context,int((n.end-n.start)*12)
|
||||
history.append(n)
|
||||
|
||||
def base(self,action):
|
||||
if action.key in self.static:return self.static[action.key]
|
||||
values=Counter();prior=Counter();ticks=int((action.end-action.start)*12)
|
||||
values['kind/'+action.kind]=ticks
|
||||
for contract in action.contracts:
|
||||
if contract.target.start>=action.end and contract.events:
|
||||
# Pending destinations must be visible to selection. Identical heard
|
||||
@@ -65,41 +115,48 @@ class Selector:
|
||||
values[f'obligation/step/{max(-12,min(12,target.pitch.number()-last.pitch.number()))}']+=ticks
|
||||
values['obligation/delay/'+str(target.start-last.start).replace('/','_')]+=ticks
|
||||
values['obligation/hold/'+str(target.end-target.start).replace('/','_')]+=ticks
|
||||
if not action.notes:
|
||||
values['space/'+('unharmonized' if action.kind=='space' else 'breath')]=ticks
|
||||
for n in action.notes:
|
||||
duration=n.end-n.start;length=int(duration*12)
|
||||
label=str(duration).replace('/','_')
|
||||
values['duration/'+label]+=length
|
||||
beat=(n.start%1)
|
||||
metric='barline' if n.start+self.analysis.body_offset in self.analysis.boundaries else 'beat' if beat==0 else 'eighth' if beat==F(1,2) else 'subdivision'
|
||||
values['metric/'+metric+'/'+label]+=length
|
||||
band=max(-4,min(4,(n.pitch.number()-self.center+6)//12))
|
||||
values[f'register/{band}']+=length
|
||||
region=self.harmony.at(n.start)
|
||||
chord=expressions.parsed_chord(region.chord)
|
||||
root=int(model.NATURAL[chord['root']['letter']]+model.alteration(chord['root']['alter']))
|
||||
degree=(n.pitch.number()-root)%12
|
||||
values[f'harmony/{expressions.quality(chord)}/{degree}/{label}']+=length
|
||||
prior['register_distance']+=length*max(0,abs(n.pitch.number()-self.center)-7)
|
||||
if duration>=2 and n.pitch.number()%12 in self.harmony.members(n.start,n.end):
|
||||
prior['sustained_arrival']+=length
|
||||
values['source/rhythm_match']+=length if duration in self.source_rhythms else 0
|
||||
f,p=self.note_base(n.start,n.end,n.pitch.number())
|
||||
values.update(f);prior.update(p)
|
||||
self.static[action.key]=(values,prior)
|
||||
if len(self.static)>8192:self.static.pop(next(iter(self.static)))
|
||||
return values,prior
|
||||
|
||||
@functools.lru_cache(maxsize=32768)
|
||||
def note_base(self,start,end,number):
|
||||
values=Counter();prior=Counter()
|
||||
duration=end-start;length=int(duration*12)
|
||||
label=str(duration).replace('/','_')
|
||||
values['duration/'+label]+=length
|
||||
beat=(start%1)
|
||||
metric='barline' if start+self.analysis.body_offset in self.analysis.boundaries else 'beat' if beat==0 else 'eighth' if beat==F(1,2) else 'subdivision'
|
||||
values['metric/'+metric+'/'+label]+=length
|
||||
band=max(-4,min(4,(number-self.center+6)//12))
|
||||
values[f'register/{band}']+=length
|
||||
region=self.harmony.at(start)
|
||||
if not region or not region.chord:return values,prior
|
||||
chord=expressions.parsed_chord(region.chord)
|
||||
root=int(model.NATURAL[chord['root']['letter']]+model.alteration(chord['root']['alter']))
|
||||
degree=(number-root)%12
|
||||
values[f'harmony/{expressions.quality(chord)}/{degree}/{label}']+=length
|
||||
prior['register_distance']+=length*max(0,abs(number-self.center)-7)
|
||||
if duration>=2 and number%12 in self.harmony.members(start,end):
|
||||
prior['sustained_arrival']+=length
|
||||
values['source/rhythm_match']+=length if duration in self.source_rhythms else 0
|
||||
return values,prior
|
||||
|
||||
def features(self,action,recent=(),home=(),previous_kind=''):
|
||||
if any(n.continuation for n in action.notes):action=dataclasses.replace(action,notes=types.sounding_notes(action.notes))
|
||||
static,base=self.base(action);values=Counter(static);prior=Counter(base)
|
||||
if not action.notes:
|
||||
if action.kind=='space':return values,prior
|
||||
ticks=int((action.end-action.start)*12)
|
||||
if previous_kind in ('rest','space'):prior['rest_after_rest']+=ticks
|
||||
elif recent and recent[-1].end-recent[-1].start>=F(3,2):values['space/after_arrival']+=ticks
|
||||
elif recent:prior['unprepared_space']+=ticks
|
||||
else:values['space/delayed_entrance']+=ticks
|
||||
return values,prior
|
||||
# Silence is measured as intervals in the sounded stream, independent of
|
||||
# the number of rest actions used to encode it. Missing harmony is distinct.
|
||||
cursor=action.start;before=recent[-1] if recent else None
|
||||
for n in (*action.notes,None):
|
||||
until=n.start if n else action.end
|
||||
if until>cursor:
|
||||
f,p=self.space_features(cursor,until,None if before is None else before.end-before.start>=F(3,2))
|
||||
values.update(f);prior.update(p)
|
||||
if n:cursor=n.end;before=n
|
||||
previous=recent[-1] if recent else None
|
||||
intervals=[b.pitch.number()-a.pitch.number() for a,b in zip(recent,recent[1:])]
|
||||
for n in action.notes:
|
||||
@@ -121,24 +178,72 @@ class Selector:
|
||||
if 0<abs(delta)<=2 and intervals and delta*intervals[-1]>0:prior['directed_motion']+=length
|
||||
intervals.append(delta)
|
||||
previous=n
|
||||
if action.kind=='return' and len(home)>=3:
|
||||
if signature(action.notes)==signature(home):
|
||||
recently_heard=any(signature(recent[i:i+len(home)])==signature(home)
|
||||
for i in range(max(0,len(recent)-len(home)+1)))
|
||||
if not recently_heard:prior['theme_relation']+=int((action.end-action.start)*12)
|
||||
if recent and home:
|
||||
if len(home)>=3:
|
||||
key=id(home)
|
||||
if key not in self.home_signatures:
|
||||
self.home_signatures[key]=(home,signature(home))
|
||||
if len(self.home_signatures)>32:self.home_signatures.pop(next(iter(self.home_signatures)))
|
||||
expected=self.home_signatures[key][1]
|
||||
joined=(*recent,*action.notes)
|
||||
count=min(len(home),len(joined))
|
||||
if count>=3 and signature(joined[-count:])==signature(home[:count]):
|
||||
values['theme/realized_identity']+=12
|
||||
for i in range(max(0,len(recent)-len(home)+1),len(joined)-len(home)+1):
|
||||
part=joined[i:i+len(home)]
|
||||
if part[0].start>home[-1].end and matches_signature(part,expected):
|
||||
values['theme/realized_identity']+=12
|
||||
prior['theme_relation']+=12
|
||||
joint=[(key,rule_conditions(key)) for key in self.weights if key.startswith('joint/')]
|
||||
if joint:
|
||||
for context,ticks in self.contexts(action.notes,recent):
|
||||
for key,conditions in joint:
|
||||
if all(context.get(k)==v for k,v in conditions.items()):values[key]+=ticks
|
||||
return values,prior
|
||||
|
||||
def complete(self,actions,home=(),*,start=None,end=None):
|
||||
"""The sole authoritative score. Ties/rests/partitions are canonicalized."""
|
||||
from generator.solo.operators import Action
|
||||
actions=tuple(actions)
|
||||
first=start if start is not None else actions[0].start
|
||||
last=end if end is not None else actions[-1].end
|
||||
notes=types.sounding_notes(tuple(n for a in actions for n in a.notes))
|
||||
contracts=tuple(dict.fromkeys(c for a in actions for c in a.contracts))
|
||||
return self.evaluate(Action(first,last,notes,'sounded_phrase',contracts),home=home)
|
||||
|
||||
def extend(self,previous,old_notes,action,home=(),*,old_home=(),start=F(),contracts=()):
|
||||
"""Exact sufficient-statistic update, checked against complete() in tests.
|
||||
|
||||
Reevaluate the seam and the tail containing any possible new home match.
|
||||
Previously completed events are not rescored on every search expansion.
|
||||
"""
|
||||
from generator.solo.operators import Action
|
||||
if previous is None or home!=old_home or any(n.continuation for n in action.notes):
|
||||
notes=types.sounding_notes((*old_notes,*action.notes))
|
||||
return self.evaluate(Action(start,action.end,notes,'sounded_phrase',contracts),home=home)
|
||||
span=F(previous.denominators['span'])
|
||||
raw=Counter({k:v*span for k,v in previous.raw.items() if not k.startswith('obligation/')})
|
||||
index=max(0,len(old_notes)-max(3,len(home)+2))
|
||||
tail=old_notes[index:];context=old_notes[max(0,index-3):index]
|
||||
begin=tail[0].start if index else start
|
||||
before=Action(begin,action.start,tail,'sounded_phrase')
|
||||
after=Action(begin,action.end,(*tail,*action.notes),'sounded_phrase')
|
||||
for sign,part in ((-1,before),(1,after)):
|
||||
f,p=self.features(part,context,home)
|
||||
for k,v in f.items():
|
||||
if not k.startswith('obligation/'):raw[k]+=sign*v
|
||||
for k,v in p.items():raw['prior/'+k]+=sign*v
|
||||
obligations,_=self.base(Action(start,action.end,(),'sounded_phrase',contracts))
|
||||
raw.update({k:v for k,v in obligations.items() if k.startswith('obligation/')})
|
||||
weights={'prior/'+k:v for k,v in DEFAULT.items()}|self.weights
|
||||
duration=action.end-start
|
||||
raw={k:F(v)/duration for k,v in raw.items() if v}
|
||||
contributions={k:v*weights.get(k,0) for k,v in raw.items()}
|
||||
return scoring.Evaluation(VERSION,True,raw,weights,contributions,sum(contributions.values(),F()),
|
||||
{'span':str(duration),'ticks_per_quarter':12,'attacks':len(old_notes)+len(action.notes)})
|
||||
|
||||
def evaluate(self,action,recent=(),home=(),previous_kind=''):
|
||||
features,prior=self.features(action,recent,home,previous_kind)
|
||||
contributions={'prior/'+k:v*DEFAULT[k] for k,v in prior.items() if v}
|
||||
contributions.update({k:v*self.weights.get(k,0) for k,v in features.items() if v})
|
||||
weights={'prior/'+k:v for k,v in DEFAULT.items()}|self.weights
|
||||
raw={'prior/'+k:v for k,v in prior.items()}|dict(features)
|
||||
raw={'prior/'+k:v for k,v in prior.items() if v}|{k:v for k,v in features.items() if v}
|
||||
span=action.end-action.start
|
||||
contributions={k:F(v)/span for k,v in contributions.items()}
|
||||
raw={k:F(v)/span for k,v in raw.items()}
|
||||
@@ -152,3 +257,110 @@ class Selector:
|
||||
numerator=sum(v*DEFAULT[k] for k,v in prior.items())
|
||||
numerator+=sum(v*self.weights.get(k,0) for k,v in features.items())
|
||||
return F(numerator)/(action.end-action.start)
|
||||
|
||||
@functools.lru_cache(maxsize=32768)
|
||||
def transition_cost(self,delta,preceding,earlier,length,label,contiguous):
|
||||
token=max(-12,min(12,delta))
|
||||
sign=lambda x:'up' if x>0 else 'down' if x<0 else 'repeat'
|
||||
cost=length*self.weights.get(f'motion/{token}/{label}',0)
|
||||
cost+=length*DEFAULT['excessive_leap']*max(0,abs(delta)-7)
|
||||
if delta==0 and contiguous:cost+=length*DEFAULT['repeated_attack']
|
||||
if preceding is not None:
|
||||
cost+=length*self.weights.get(f'continuation/{sign(preceding)}/{sign(delta)}',0)
|
||||
if (preceding,delta) in self.source_pairs:cost+=length*DEFAULT['source_relation']
|
||||
if 0<abs(delta)<=2 and delta*preceding>0:cost+=length*DEFAULT['directed_motion']
|
||||
if earlier is not None and earlier==-preceding==delta and delta:cost+=length*DEFAULT['oscillation']
|
||||
return cost
|
||||
|
||||
@functools.lru_cache(maxsize=32768)
|
||||
def seam_cost(self,prefix,context,contiguous):
|
||||
def total(initial):
|
||||
pitches=list(initial);value=0
|
||||
for index,(number,length,label,touch) in enumerate(prefix):
|
||||
if pitches:
|
||||
value+=self.transition_cost(number-pitches[-1],pitches[-1]-pitches[-2] if len(pitches)>1 else None,
|
||||
pitches[-2]-pitches[-3] if len(pitches)>2 else None,length,label,contiguous if index==0 and initial else touch)
|
||||
pitches.append(number)
|
||||
return value
|
||||
return total(context)-total(())
|
||||
|
||||
@functools.lru_cache(maxsize=32768)
|
||||
def unary_cost(self,start,end,number):
|
||||
features,prior=self.note_base(start,end,number)
|
||||
return sum(v*DEFAULT[k] for k,v in prior.items())+sum(v*self.weights.get(k,0) for k,v in features.items())
|
||||
|
||||
def intrinsic_cost(self,action):
|
||||
"""Exact ordinary retrieval cost without constructing discarded reports.
|
||||
|
||||
Unary, transition, silence and pending-target contributions are identical
|
||||
to features(action). Contextual learned conjunctions use the full path.
|
||||
"""
|
||||
notes=types.sounding_notes(action.notes) if any(n.continuation for n in action.notes) else action.notes
|
||||
cost=0;history=[];cursor=action.start;settled=None;previous=None
|
||||
for n in notes:
|
||||
if n.start>cursor:cost+=self.gap_cost(cursor,n.start,settled)
|
||||
number=n.pitch.number();length=int((n.end-n.start)*12);label=str(n.end-n.start).replace('/','_')
|
||||
cost+=self.unary_cost(n.start,n.end,number)
|
||||
if history:
|
||||
cost+=self.transition_cost(number-history[-1],history[-1]-history[-2] if len(history)>1 else None,
|
||||
history[-2]-history[-3] if len(history)>2 else None,length,label,n.start==previous.end)
|
||||
history.append(number);previous=n;cursor=n.end;settled=n.end-n.start>=F(3,2)
|
||||
if action.end>cursor:cost+=self.gap_cost(cursor,action.end,settled)
|
||||
ticks=int((action.end-action.start)*12)
|
||||
for contract in action.contracts:
|
||||
if contract.target.start>=action.end and contract.events:
|
||||
last=contract.events[-1];target=contract.target
|
||||
cost+=ticks*self.weights.get(f'obligation/step/{max(-12,min(12,target.pitch.number()-last.pitch.number()))}',0)
|
||||
cost+=ticks*self.weights.get('obligation/delay/'+str(target.start-last.start).replace('/','_'),0)
|
||||
cost+=ticks*self.weights.get('obligation/hold/'+str(target.end-target.start).replace('/','_'),0)
|
||||
return cost
|
||||
|
||||
def fast_cost(self,action,recent=(),context=None):
|
||||
"""Unnormalized endpoint-bucket retrieval, with cached internal routes.
|
||||
|
||||
Only the first three transitions depend on incoming history. Final ranking
|
||||
always uses complete(); this heuristic does not compare unequal spans.
|
||||
"""
|
||||
# Learned conjunctions include incoming context; do not use a shortcut
|
||||
# that forgets it. This small fallback is explicit and profileable.
|
||||
if any(n.continuation for n in action.notes) or any(k.startswith('joint/') for k in self.weights):
|
||||
f,p=self.features(action,recent)
|
||||
return sum(v*DEFAULT[k] for k,v in p.items())+sum(v*self.weights.get(k,0) for k,v in f.items())
|
||||
if self.cost_weights is not self.weights:
|
||||
self.cost_static.clear();self.transition_cost.cache_clear();self.seam_cost.cache_clear();self.gap_cost.cache_clear();self.unary_cost.cache_clear();self.cost_weights=self.weights
|
||||
key=id(action)
|
||||
if key not in self.cost_static:
|
||||
value=self.intrinsic_cost(action)
|
||||
prefix=tuple((n.pitch.number(),int((n.end-n.start)*12),str(n.end-n.start).replace('/','_'),
|
||||
i>0 and action.notes[i-1].end==n.start) for i,n in enumerate(action.notes[:3]))
|
||||
self.cost_static[key]=(action,value,prefix)
|
||||
if len(self.cost_static)>32768:self.cost_static.pop(next(iter(self.cost_static)))
|
||||
_,value,prefix=self.cost_static[key]
|
||||
if context is None:context=(tuple(n.pitch.number() for n in recent[-3:]),recent[-1].end if recent else None)
|
||||
if context[0] and prefix:value+=self.seam_cost(prefix,context[0],context[1]==action.notes[0].start)
|
||||
gap_end=action.notes[0].start if action.notes else action.end
|
||||
if recent and gap_end>action.start:
|
||||
# Incoming sustained arrivals change the role of a leading rest, just
|
||||
# as incoming pitches change the first intervals. Do not cache it as a
|
||||
# fresh silent entrance in every action.
|
||||
value+=self.gap_cost(action.start,gap_end,recent[-1].end-recent[-1].start>=F(3,2))-self.gap_cost(action.start,gap_end,None)
|
||||
return value
|
||||
|
||||
@functools.lru_cache(maxsize=8192)
|
||||
def space_features(self,start,end,settled):
|
||||
values=Counter();prior=Counter();delayed=0
|
||||
boundaries=sorted({start,end,*[s.start for s in self.harmony.spans if start<s.start<end],
|
||||
*[s.end for s in self.harmony.spans if start<s.end<end]})
|
||||
for left,right in zip(boundaries,boundaries[1:]):
|
||||
ticks=int((right-left)*12);region=self.harmony.at(left)
|
||||
label='unharmonized' if not region or not region.scale else 'delayed_entrance' if settled is None else 'after_arrival' if settled else 'breath'
|
||||
values['space/'+label]+=ticks
|
||||
if label=='breath':prior['unprepared_space']+=ticks
|
||||
if label=='delayed_entrance':delayed+=ticks
|
||||
prior['unprepared_space']+=max(0,delayed-12)
|
||||
return values,prior
|
||||
|
||||
@functools.lru_cache(maxsize=8192)
|
||||
def gap_cost(self,start,end,settled):
|
||||
f,p=self.space_features(start,end,settled)
|
||||
return sum(v*DEFAULT[k] for k,v in p.items())+sum(v*self.weights.get(k,0) for k,v in f.items())
|
||||
|
||||
Reference in New Issue
Block a user