Playback
ci / checks (push) Failing after 23m37s

This commit is contained in:
khodowany
2026-10-05 23:41:11 -05:00
parent ffdbe48258
commit 82349cf897
104 changed files with 4095 additions and 4354 deletions
+133 -107
View File
@@ -4,138 +4,164 @@ No filenames, annotations, learned examples or role exemptions enter here.
Existing expression validators prove each candidate; deterministic set-cover
search chooses a mutually consistent explanation, never edits the notes.
"""
from fractions import Fraction as F
from generator import vocabulary
from generator.solo import expressions, types
import fractions
import generator.vocabulary
import generator.solo.expressions
import generator.solo.types
def signature(c):
return (c.kind,c.rule,tuple(n.event_id for n in c.events),c.target.event_id,
str(c.boundary),c.preparation.event_id if c.preparation else '')
return (c.kind, c.rule, tuple(n.event_id for n in c.events), c.target.event_id,
str(c.boundary), c.preparation.event_id if c.preparation else '')
def merge(*groups):
result={}
result = {}
for group in groups:
for c in group:result.setdefault(signature(c),c)
for c in group:
result.setdefault(signature(c), c)
return tuple(result.values())
def recognize(notes, harmony, provided=(), *, budget=4096):
required = {n.event_id for n in notes if not harmony.ordinary(n)}
fixed = {n.event_id for c in provided for n in c.events}
if required <= fixed:
return tuple(provided), ''
candidates = {}
single = {}
by_id = {n.event_id: n for n in notes}
if len(by_id) != len(notes):
return (), 'Harmonic analysis requires unique sounding-event IDs'
def recognize(notes,harmony,provided=(),*,budget=4096):
required={n.event_id for n in notes if not harmony.ordinary(n)}
fixed={n.event_id for c in provided for n in c.events}
if required<=fixed:return tuple(provided),''
candidates={};single={};by_id={n.event_id:n for n in notes}
if len(by_id)!=len(notes):return (), 'Harmonic analysis requires unique sounding-event IDs'
def contract(kind, rule, events, target, boundary=None, preparation=None):
span = harmony.at(boundary if boundary is not None else target.start)
if not span or span.chord == 'N.C.':
return None
identifier = f'inferred:{kind}:{rule}:{events[0].event_id}:{target.event_id}:{boundary}'
return generator.solo.types.ExpressionContract(identifier, kind, rule, tuple(events), target, span.chord, boundary, preparation)
def contract(kind,rule,events,target,boundary=None,preparation=None):
span=harmony.at(boundary if boundary is not None else target.start)
if not span or span.chord=='N.C.':return None
identifier=f'inferred:{kind}:{rule}:{events[0].event_id}:{target.event_id}:{boundary}'
return types.ExpressionContract(identifier,kind,rule,tuple(events),target,span.chord,boundary,preparation)
def add(c,extras=()):
if c is None:return False
bundle=merge(extras,(c,));covered=frozenset(n.event_id for a in bundle for n in a.events)
if not (covered & required):return False
combined=merge(provided,bundle)
_,error=expressions.validate(notes,combined,harmony)
if error:return False
candidates[tuple(signature(a) for a in bundle)]=(bundle,covered)
def add(c, extras=()):
if c is None:
return False
bundle = merge(extras, (c,))
covered = frozenset(n.event_id for a in bundle for n in a.events)
if not (covered & required):
return False
combined = merge(provided, bundle)
_, error = generator.solo.expressions.validate(notes, combined, harmony)
if error:
return False
candidates[tuple(signature(a) for a in bundle)] = (bundle, covered)
return True
# First discover independently supported colors and harmonic anticipations.
# These can also support a later approach's destination or passing endpoint.
for i,n in enumerate(notes):
if n.event_id not in required:continue
future=[t for t in notes[i+1:] if n.end<=t.start<=n.end+F(vocabulary.data('harmony')['resolution_gap'])
and 0<abs(t.pitch.number()-n.pitch.number())<=2]
regions=[(None,harmony.at(n.start))]
regions.extend((s.start,s) for s in harmony.spans if n.start<s.start<n.end
and s.start-n.start<=F(vocabulary.data('harmony')['anticipation_max']))
for boundary,span in regions:
if not span or span.chord=='N.C.':continue
for i, n in enumerate(notes):
if n.event_id not in required:
continue
future = [t for t in notes[i + 1:] if n.end <= t.start <= n.end + fractions.Fraction(generator.vocabulary.data('harmony')['resolution_gap'])
and 0 < abs(t.pitch.number() - n.pitch.number()) <= 2]
regions = [(None, harmony.at(n.start))]
regions.extend((s.start, s) for s in harmony.spans if n.start < s.start < n.end
and s.start - n.start <= fractions.Fraction(generator.vocabulary.data('harmony')['anticipation_max']))
for boundary, span in regions:
if not span or span.chord == 'N.C.':
continue
if boundary is not None:
c=contract('anticipation','early_chord_tone',(n,),n,boundary)
if add(c):single.setdefault(n.event_id,[]).append((c,))
for rule in vocabulary.data('harmony')['colors']:
if not expressions.eligible_color(rule,span,harmony) and boundary is not None:continue
kind='anticipated_color' if boundary is not None else 'color'
for target in future if rule.get('resolve') else (n,):
c=contract(kind,rule['id'],(n,),target,boundary)
c = contract('anticipation', 'early_chord_tone', (n,), n, boundary)
if add(c):
single.setdefault(n.event_id, []).append((c,))
for rule in generator.vocabulary.data('harmony')['colors']:
if not generator.solo.expressions.eligible_color(rule, span, harmony) and boundary is not None:
continue
kind = 'anticipated_color' if boundary is not None else 'color'
for target in future if rule.get('resolve') else [n]:
c = contract(kind, rule['id'], (n,), target, boundary)
# Non-anticipated color permission is scoped to the note's initial
# harmony, even when its resolution occurs after a chord boundary.
if boundary is None and c:c=types.ExpressionContract(c.id,c.kind,c.rule,c.events,c.target,span.chord)
if add(c):single.setdefault(n.event_id,[]).append((c,))
if i+1<len(notes):
target=notes[i+1]
if boundary is None and c:
c = generator.solo.types.ExpressionContract(c.id, c.kind, c.rule, c.events, c.target, span.chord)
if add(c):
single.setdefault(n.event_id, []).append((c,))
if i + 1 < len(notes):
target = notes[i + 1]
for s in harmony.spans:
if n.start<s.start<n.end:
for rule in vocabulary.data('harmony').get('suspensions',[]):
add(contract('suspension',rule['id'],(n,),target,s.start))
elif s.start==n.end==target.start and n.pitch.number()==target.pitch.number():
add(contract('anticipation','early_reattack',(n,),target,s.start))
for i,target in enumerate(notes):
targets=[(None,())]
for support in single.get(target.event_id,()):
c=support[0]
if n.start < s.start < n.end:
for rule in generator.vocabulary.data('harmony').get('suspensions', []):
add(contract('suspension', rule['id'], (n,), target, s.start))
elif s.start == n.end == target.start and n.pitch.number() == target.pitch.number():
add(contract('anticipation', 'early_reattack', (n,), target, s.start))
for i, target in enumerate(notes):
targets = [(None, ())]
for support in single.get(target.event_id, ()):
c = support[0]
# An approach into an anticipated color needs both permissions; that
# combination is not silently interpreted as a chord-tone anticipation.
if c.kind=='color':targets.append((None,support))
elif c.kind=='anticipation':targets.append((c.boundary,support))
for rule in vocabulary.data('lead_ins')['patterns']:
size=len(rule['semitones'])
if i<size:continue
ns=notes[i-size:i]
if not any(n.event_id in required for n in ns):continue
if tuple(n.pitch.number()-target.pitch.number() for n in ns)!=tuple(rule['semitones']):continue
preparation=notes[i-size-1] if i>size and rule['kind']=='connection' else None
for boundary,extras in targets:
add(contract(rule['kind'],rule['id'],ns,target,boundary,preparation),extras)
if c.kind == 'color':
targets.append((None, support))
elif c.kind == 'anticipation':
targets.append((c.boundary, support))
for rule in generator.vocabulary.data('lead_ins')['patterns']:
size = len(rule['semitones'])
if i < size:
continue
ns = notes[i - size:i]
if not any(n.event_id in required for n in ns):
continue
if tuple(n.pitch.number() - target.pitch.number() for n in ns) != tuple(rule['semitones']):
continue
preparation = notes[i - size - 1] if i > size and rule['kind'] == 'connection' else None
for boundary, extras in targets:
add(contract(rule['kind'], rule['id'], ns, target, boundary, preparation), extras)
# Delayed approaches may include real space or intervening valid music.
# They still have an exact semitone target and a finite deadline.
rule=vocabulary.data('lead_ins')['delayed_approach']
rule = generator.vocabulary.data('lead_ins')['delayed_approach']
for n in reversed(notes[:i]):
if target.start-n.start>F(rule['deadline']):break
if n.event_id in required and target.pitch.number()-n.pitch.number() in rule['steps']:
add(contract('delayed_approach',rule['id'],(n,),target))
if i>=2:
before,n=notes[i-2:i]
if target.start - n.start > fractions.Fraction(rule['deadline']):
break
if n.event_id in required and target.pitch.number() - n.pitch.number() in rule['steps']:
add(contract('delayed_approach', rule['id'], (n,), target))
if i >= 2:
before, n = notes[i - 2:i]
if n.event_id in required:
for left in [(),*single.get(before.event_id,())]:
for right in [(),*single.get(target.event_id,())]:
add(contract('passing',vocabulary.data('lead_ins')['passing']['id'],(n,),target,preparation=before),merge(left,right))
for rule in vocabulary.data('lead_ins')['blues']:
add(contract('blues',rule['id'],(n,),target,preparation=before))
options={identifier:[] for identifier in sorted(required-fixed,key=lambda identifier:(by_id[identifier].start,identifier))}
for bundle,covered in candidates.values():
for identifier in covered & options.keys():options[identifier].append((bundle,covered))
for identifier,choices in options.items():
for left in [(), *single.get(before.event_id, ())]:
for right in [(), *single.get(target.event_id, ())]:
add(contract('passing', generator.vocabulary.data('lead_ins')['passing']['id'], (n,), target, preparation=before), merge(left, right))
for rule in generator.vocabulary.data('lead_ins')['blues']:
add(contract('blues', rule['id'], (n,), target, preparation=before))
options = {identifier: [] for identifier in sorted(required - fixed, key=lambda identifier: (by_id[identifier].start, identifier))}
for bundle, covered in candidates.values():
for identifier in covered & options.keys():
options[identifier].append((bundle, covered))
for identifier, choices in options.items():
if not choices:
n=by_id[identifier]
return (),f'{identifier} at {n.start}: unsupported harmonic relationship for {n.pitch.label()}'
choices.sort(key=lambda x:(sum((n.end-n.start for c in x[0] for n in c.events),F()),
sum((max(F(),c.target.start-c.events[-1].end) for c in x[0]),F()),tuple(signature(c) for c in x[0])))
n = by_id[identifier]
return (), f'{identifier} at {n.start}: unsupported harmonic relationship for {n.pitch.label()}'
choices.sort(key=lambda x: (sum((n.end - n.start for c in x[0] for n in c.events), fractions.Fraction()),
sum((max(fractions.Fraction(), c.target.start - c.events[-1].end) for c in x[0]), fractions.Fraction()), tuple(signature(c) for c in x[0])))
visits = 0
failed = set()
visits=0;failed=set()
def solve(chosen,covered):
def solve(chosen, covered):
nonlocal visits
remaining=required-covered
if not remaining:return chosen
key=tuple(sorted(signature(c) for c in chosen))
if key in failed or visits>=budget:return None
visits+=1
identifier=min(remaining,key=lambda x:(len(options[x]),by_id[x].start,x))
for bundle,added in options[identifier]:
combined=merge(chosen,bundle)
_,error=expressions.validate(notes,combined,harmony)
if error:continue
result=solve(combined,covered|added)
if result is not None:return result
remaining = required - covered
if not remaining:
return chosen
key = tuple(sorted(signature(c) for c in chosen))
if key in failed or visits >= budget:
return None
visits += 1
identifier = min(remaining, key=lambda x: (len(options[x]), by_id[x].start, x))
for bundle, added in options[identifier]:
combined = merge(chosen, bundle)
_, error = generator.solo.expressions.validate(notes, combined, harmony)
if error:
continue
result = solve(combined, covered | added)
if result is not None:
return result
failed.add(key)
return None
result=solve(tuple(provided),fixed)
if result is None:return (), 'Harmonic treatment search budget exhausted' if visits>=budget else 'No mutually consistent harmonic treatment coverage'
return result,''
result = solve(tuple(provided), fixed)
if result is None:
return (), 'Harmonic treatment search budget exhausted' if visits >= budget else 'No mutually consistent harmonic treatment coverage'
return result, ''