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
+71 -38
View File
@@ -31,11 +31,34 @@ def candidates(notes, fixed, harmony, library, register, relationship='connect t
if not hasattr(harmony,'route_cache'):
harmony.route_cache={}
harmony.route_libraries={}
harmony.concrete_routes={}
harmony.route_libraries[id(library)]=library
key=(notes,frozenset(fixed),id(library),register)
concrete_key=(notes,frozenset(fixed),id(library),register)
if concrete_key in harmony.concrete_routes:
return tuple(dataclasses.replace(r,relationship=relationship) for r in harmony.concrete_routes[concrete_key])
# Event/source IDs and non-fixed placeholder pitches do not affect route
# feasibility. Cache the pitch work across different thematic histories,
# then restore this caller's provenance before ranking complete routes.
fixed_indices=frozenset(i for i,n in enumerate(notes) if n.event_id in fixed)
canonical=tuple(dataclasses.replace(n,sources=(),event_id=str(i),role='connection',position=None,
pitch=n.pitch if i==0 or i==len(notes)-1 or i in fixed_indices else types.Pitch('c',fractions.Fraction(),0)) for i,n in enumerate(notes))
key=(canonical,fixed_indices,id(library),register)
if key not in harmony.route_cache:
harmony.route_cache[key]=_candidates(notes,fixed,harmony,library,register)
return tuple(dataclasses.replace(r,relationship=relationship) for r in harmony.route_cache[key])
harmony.route_cache[key]=_candidates(canonical,{str(i) for i in fixed_indices},harmony,library,register)
result=[]
for route in harmony.route_cache[key]:
actual=tuple(n if i==0 else dataclasses.replace(n,pitch=n.pitch if n.pitch.number()==r.pitch.number() else r.pitch,position=None)
for i,(n,r) in enumerate(zip(notes,route.notes)))
identity=generator.solo.policy.stable_rank('complete-route',(actual,route.sources))[:16]
result.append(dataclasses.replace(route,id=identity,notes=actual,destination=notes[-1],relationship=relationship))
selected=generator.solo.sequences.diverse(result,
lambda r:(sum(abs(b.pitch.number()-a.pitch.number()) for a,b in zip(r.notes,r.notes[1:])),r.id),
lambda r:generator.solo.sequences.family(tuple(b.pitch.number()-a.pitch.number() for a,b in zip(r.notes,r.notes[1:]))),
generator.solo.policy.DEFAULT.join_alternatives)
if len(harmony.concrete_routes)>=8192:harmony.concrete_routes.pop(next(iter(harmony.concrete_routes)))
if len(harmony.route_cache)>=8192:harmony.route_cache.pop(next(iter(harmony.route_cache)))
harmony.concrete_routes[concrete_key]=selected
return selected
def _candidates(notes, fixed, harmony, library, register):
"""Every returned candidate includes its entry, complete rhythm and destination.
@@ -49,41 +72,51 @@ def _candidates(notes, fixed, harmony, library, register):
span = harmony.at(first.start)
if not span or not span.scale:
return ()
contexts=tuple((harmony.at(n.start).scale if harmony.at(n.start) else None,
types.permitted(harmony.choices,n.start,n.end),
n.pitch.number() if n.event_id in fixed or i==len(notes)-1 else None) for i,n in enumerate(notes))
if not hasattr(harmony,'pitch_routes'):harmony.pitch_routes={}
key=(first.pitch,contexts,id(library),register)
if key not in harmony.pitch_routes:
harmony.pitch_routes[key]=pitch_paths(first.pitch,contexts,library,register)
if len(harmony.pitch_routes)>8192:harmony.pitch_routes.pop(next(iter(harmony.pitch_routes)))
route_spans=tuple((max(first.start,c.span.start),min(destination.end,c.span.end),c.scale.label())
for c in harmony.choices if c.scale and c.span.start<destination.end and c.span.end>first.start)
result=[]
for pitches,sources in harmony.pitch_routes[key]:
realized=(first,*(dataclasses.replace(n,pitch=n.pitch if n.pitch.number()==p.number() else p,position=None)
for n,p in zip(notes[1:],pitches[1:])))
identity=generator.solo.policy.stable_rank('complete-route',(realized,sources))[:16]
result.append(types.RouteCandidate(identity,realized,destination,route_spans,'connect theme',sources))
return tuple(result)
def pitch_paths(first, contexts, library, register):
"""Time-independent pitch work; context includes every overlapped collection.
Sounding durations and chord roles are still validated on each concrete
realization. Equal collection contexts can reuse paths across rhythmic edits.
"""
destination=contexts[-1][2]
try:
displacement = types.scale_index(span.scale, destination.pitch.number()) - types.scale_index(span.scale, first.pitch.number())
displacements = tuple([displacement])
displacement=types.scale_index(contexts[0][0],destination)-types.scale_index(contexts[0][0],first.number())
displacements=(displacement,)
except ValueError:
# Enumerate possible degree routes when the destination changes collection;
# actual endpoint equality below decides compatibility, not a pitch estimate.
displacements = tuple(range(-7, 8))
direction = (destination.pitch.number() > first.pitch.number()) - (destination.pitch.number() < first.pitch.number())
paths = dict.fromkeys(path for d in displacements for path in library.retrieve(len(notes), d, direction))
result = []
low, high = generator.fretboard.single_register((register[0] - 7, register[1] + 5))
allowed = {n.start: types.permitted(harmony.choices, n.start, n.end) for n in notes}
displacements=tuple(range(-7,8))
direction=(destination>first.number())-(destination<first.number())
paths=dict.fromkeys(path for d in displacements for path in library.retrieve(len(contexts),d,direction))
low,high=generator.fretboard.single_register((register[0]-7,register[1]+5))
result=[]
for path in paths:
realized = [first]
for template, step in zip(notes[1:], path.steps):
current = harmony.at(template.start)
if not current or not current.scale:
break
options = advance(current.scale, realized[-1].pitch.number(), step)
if not options:
break
pitch = options[0]
if not low <= pitch.number() <= high or pitch.number() % 12 not in allowed[template.start]:
break
if (template.event_id in fixed or template is destination) and pitch.number() != template.pitch.number():
break
if not generator.solo.placement.positions(pitch.number()):
break
realized.append(dataclasses.replace(template, pitch=template.pitch if pitch.number() == template.pitch.number() else pitch, position=None))
if len(realized) != len(notes):
continue
route_spans = tuple((max(first.start, c.span.start), min(destination.end, c.span.end), c.scale.label()) for c in harmony.choices if c.scale and c.span.start < destination.end and c.span.end > first.start)
identity = generator.solo.policy.stable_rank('complete-route', (tuple(realized), path.sources))[:16]
result.append(types.RouteCandidate(identity, tuple(realized), destination, route_spans, 'connect theme', path.sources))
return generator.solo.sequences.diverse(result,
lambda r: (sum(abs(b.pitch.number() - a.pitch.number()) for a, b in zip(r.notes, r.notes[1:])), r.id),
lambda r: generator.solo.sequences.family(tuple(b.pitch.number() - a.pitch.number() for a, b in zip(r.notes, r.notes[1:]))),
generator.solo.policy.DEFAULT.join_alternatives)
realized=[first]
for (scale,allowed,fixed),step in zip(contexts[1:],path.steps):
if not scale:break
options=advance(scale,realized[-1].number(),step)
if not options:break
pitch=options[0];number=pitch.number()
if not low<=number<=high or number%12 not in allowed:break
if fixed is not None and number!=fixed:break
if not generator.solo.placement.positions(number):break
realized.append(pitch)
if len(realized)==len(contexts):result.append((tuple(realized),path.sources))
return tuple(result)