Cleanup
This commit is contained in:
@@ -41,7 +41,9 @@ def advance(scale, previous, step):
|
||||
outgoing.reverse()
|
||||
return tuple(outgoing[abs(step) - 1:abs(step)])
|
||||
|
||||
def candidates(notes, fixed, harmony, library, register, relationship='connect theme', *, purpose='connect'):
|
||||
def candidates(notes, fixed, harmony, library, register, relationship='connect theme', *, purpose='connect', expressions=()):
|
||||
if expressions:
|
||||
return planned_candidates(notes,fixed,harmony,register,relationship,expressions)
|
||||
# Cache concrete routes only with their endpoints, timing, scale context and
|
||||
# vocabulary. The per-Harmony cache cannot leak across collection changes.
|
||||
if not hasattr(harmony,'route_cache'):
|
||||
@@ -76,6 +78,80 @@ def candidates(notes, fixed, harmony, library, register, relationship='connect t
|
||||
harmony.concrete_routes[concrete_key]=selected
|
||||
return selected
|
||||
|
||||
|
||||
def planned_candidates(notes,fixed,harmony,register,relationship,contracts):
|
||||
# Cache complete contract-conditioned pitch work, not a next-note suggestion.
|
||||
# IDs/provenance are restored below; timing, pitch, role, obligations and
|
||||
# profile remain in the key. The cache is owned by this harmonic timeline.
|
||||
ids={n.event_id:str(i) for i,n in enumerate(notes)}
|
||||
canonical=tuple(dataclasses.replace(n,sources=(),event_id=str(i)) for i,n in enumerate(notes))
|
||||
def note(n):return canonical[int(ids[n.event_id])]
|
||||
obligations=tuple(dataclasses.replace(c,id=str(i),events=tuple(map(note,c.events)),
|
||||
target=note(c.target),preparation=note(c.preparation) if c.preparation else None) for i,c in enumerate(contracts))
|
||||
locked=frozenset(ids[i] for i in fixed)
|
||||
key=(canonical,locked,register,harmony.profile,obligations)
|
||||
if not hasattr(harmony,'planned_route_cache'):harmony.planned_route_cache={}
|
||||
if key not in harmony.planned_route_cache:
|
||||
harmony.planned_route_cache[key]=_planned_candidates(canonical,locked,harmony,register,'',obligations)
|
||||
if len(harmony.planned_route_cache)>8192:harmony.planned_route_cache.pop(next(iter(harmony.planned_route_cache)))
|
||||
result=[]
|
||||
for route in harmony.planned_route_cache[key]:
|
||||
actual=tuple(dataclasses.replace(original,pitch=realized.pitch,position=realized.position) for original,realized in zip(notes,route.notes))
|
||||
result.append(dataclasses.replace(route,notes=actual,destination=actual[-1],relationship=relationship))
|
||||
return tuple(result)
|
||||
|
||||
|
||||
def _planned_candidates(notes,fixed,harmony,register,relationship,contracts):
|
||||
"""Solve the whole route around explicit, preselected harmonic obligations.
|
||||
|
||||
Fixed expressive pitches have narrow permissions; all other attacks use the
|
||||
map. Complete contracts are checked after realization, never inferred merely
|
||||
from a label. No incomplete route is handed back to the next cell.
|
||||
"""
|
||||
from generator.solo import gestures
|
||||
proposal=gestures.Proposal(notes,contracts,relationship)
|
||||
locked={n.event_id for c in contracts for n in (*c.events,c.target)}|set(fixed)|{notes[0].event_id,notes[-1].event_id}
|
||||
special={n.event_id for c in contracts for n in c.events}
|
||||
states=[(0.,(notes[0],))]
|
||||
for i,n in enumerate(notes[1:],1):
|
||||
span=harmony.at(n.start)
|
||||
if not span or not span.scale:return ()
|
||||
allowed=types.permitted(harmony.choices,n.start,n.end)
|
||||
if n.end-n.start>=2 and n.event_id not in special:allowed &= harmony.members(n.start,n.end)
|
||||
target=next((a for a in notes[i:] if a.event_id in locked),notes[-1])
|
||||
remaining=max(1,sum(a.start<target.start for a in notes[i:]))
|
||||
following=[]
|
||||
for cost,path in states:
|
||||
prior=path[-1]
|
||||
if n.event_id in locked:options=(n.pitch,)
|
||||
else:
|
||||
options=tuple(p for step in (-3,-2,-1,1,2,3) for p in advance(span.scale,prior.pitch.number(),step))
|
||||
if prior.pitch.number()%12 in allowed:options=(*options,prior.pitch)
|
||||
for pitch in options:
|
||||
number=pitch.number()
|
||||
if not harmony.profile.permits(number) or not register[0]<=number<=register[1]:continue
|
||||
if n.event_id not in special and number%12 not in allowed:continue
|
||||
motion=number-prior.pitch.number()
|
||||
if abs(motion)>12:continue
|
||||
# Continuation aims at the known future destination. Selected thirds
|
||||
# and fourths remain feasible; repeated oscillation is not progress.
|
||||
distance=abs(target.pitch.number()-number)
|
||||
remaining_cost=max(0,distance-5*remaining)/12
|
||||
repeating=len(path)>1 and path[-2].pitch==prior.pitch==pitch
|
||||
value=cost+max(0,abs(motion)-7)/12+.2*repeating
|
||||
following.append((value,(*path,dataclasses.replace(n,pitch=pitch,position=None)),remaining_cost))
|
||||
chosen=generator.solo.sequences.diverse(following,lambda v:(v[0]+v[2],tuple(n.pitch.number() for n in v[1])),
|
||||
lambda v:(generator.solo.sequences.family(tuple(b.pitch.number()-a.pitch.number() for a,b in zip(v[1],v[1][1:]))),v[1][-1].pitch.number()),8)
|
||||
states=[(cost,path) for cost,path,_ in chosen]
|
||||
if not states:return ()
|
||||
result=[]
|
||||
for _,actual in states:
|
||||
bound=proposal.bind(actual)
|
||||
if not harmony.evaluate(actual,until=actual[-1].end,complete=True,expressions=bound).valid:continue
|
||||
identity=generator.solo.policy.stable_rank('planned-route',tuple((n.start,n.end,n.pitch) for n in actual))[:16]
|
||||
result.append(types.RouteCandidate(identity,actual,actual[-1],(),relationship,tuple('gesture:'+c.rule for c in bound)))
|
||||
return tuple(result)
|
||||
|
||||
def _candidates(notes, fixed, harmony, library, register, purpose='connect'):
|
||||
"""Every returned candidate includes its entry, complete rhythm and destination.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user