+16
-18
@@ -15,7 +15,7 @@ import generator.solo.policy
|
||||
import generator.solo.realization
|
||||
import generator.solo.types
|
||||
|
||||
def compose(analysis, measures, part, end, *, scales, sequences, rhythms, initial=None):
|
||||
def compose(analysis, measures, part, end, *, scales, sequences, rhythms, initial=None, revise=True, context=None):
|
||||
timeline = generator.scales.timeline.read({'measures': measures}, initial=initial)
|
||||
region = generator.scales.timeline.Solo(fractions.Fraction(), end, part.capitalize() + ' Solo')
|
||||
choices = generator.scales.selection.select(timeline, region, scales)
|
||||
@@ -29,27 +29,21 @@ def compose(analysis, measures, part, end, *, scales, sequences, rhythms, initia
|
||||
if not all(pc <= generator.solo.types.permitted(choices, start, finish) for start, finish, pc in retained):
|
||||
raise ValueError(f'{part} solo: protected quotation has no exact compatible scale collection')
|
||||
choices = tuple(dataclasses.replace(c, reason=c.reason + '; retained quotation') if any(a < c.span.end and b > c.span.start for a, b, _ in retained) else c for c in choices)
|
||||
context = generator.solo.realization.SearchContext()
|
||||
context = context if context is not None else generator.solo.realization.SearchContext()
|
||||
context.revise = revise
|
||||
|
||||
def realize(plan, previous):
|
||||
original = plan
|
||||
for attempt in range(generator.solo.policy.DEFAULT.replan_attempts + 1):
|
||||
context.batch = attempt
|
||||
try:
|
||||
options = generator.solo.realization.alternatives(plan, measures, choices, sequences, rhythms, analysis.tempo, previous, context)
|
||||
if attempt:
|
||||
context.results[(original, previous)] = options
|
||||
return options
|
||||
except generator.solo.realization.SearchExhausted as error:
|
||||
if attempt == generator.solo.policy.DEFAULT.replan_attempts or plan.treatment in ('quote', 'exact_quote'):
|
||||
raise
|
||||
context.diagnostics.setdefault('replanning', []).append({'phrase': plan.id, 'attempt': attempt + 1, 'reason': str(error), 'adjustment': 'contour reference' if attempt == 0 else 'opening reference'})
|
||||
# Replan only ordinary development contracts; harmony, anchor pitches,
|
||||
# required holds, timing, activity and protected quotations stay fixed.
|
||||
treatment = 'contour_reference' if attempt == 0 else 'opening_reference'
|
||||
gestures = tuple(dataclasses.replace(g, treatment=treatment) if g.relationship not in ('statement', 'close') else g for g in plan.gestures)
|
||||
plan = dataclasses.replace(plan, gestures=gestures)
|
||||
context.diagnostics.setdefault('replanning', []).append({'phrase': plan.id, 'attempt': attempt + 1, 'reason': str(error), 'adjustment': 'next diverse complete rhythm and anchor constructions; contracts unchanged'})
|
||||
|
||||
phrases = choose_phrases(plans, realize)
|
||||
phrases = choose_phrases(plans, realize, lambda first, second, incoming: generator.solo.realization.revise_pair(first, second, incoming, choices, sequences, analysis.tempo, context))
|
||||
context.diagnostics['policy'] = generator.solo.policy.describe()['fingerprint']
|
||||
context.diagnostics['musical_identity'] = generator.solo.analysis.identity(analysis, measures, part)
|
||||
passage_ids = tuple(dict.fromkeys(p.passage_id for p in plans if p.passage_id))
|
||||
@@ -77,18 +71,20 @@ def transition_cost(first, second):
|
||||
repeated_answer = second.plan.relationship == 'answer' and before_shape == after_shape
|
||||
return min(1, float(gap) / 2) + 0.5 * min(1, max(0, jump - 5) / 12) + 0.5 * repeated_answer
|
||||
|
||||
def choose_phrases(plans, realize):
|
||||
def choose_phrases(plans, realize, revise_pair=None):
|
||||
"""Commit only after the next phrase has been evaluated from each ending."""
|
||||
result, previous = [], None
|
||||
result, previous, pending = [], None, None
|
||||
for index, plan in enumerate(plans):
|
||||
options = realize(plan, previous)
|
||||
if pending is not None and pending not in options:
|
||||
options = (pending, *options)
|
||||
ranked, failure = [], None
|
||||
for option in options:
|
||||
notes = phrase_notes(option)
|
||||
last = notes[-1] if notes else previous
|
||||
incoming = transition_cost(result[-1], option) if result else 0
|
||||
if index + 1 == len(plans):
|
||||
ranked.append((option.score + incoming, option))
|
||||
ranked.append((option.score + incoming, option, None))
|
||||
continue
|
||||
try:
|
||||
following = realize(plans[index + 1], option.state or last)
|
||||
@@ -98,11 +94,13 @@ def choose_phrases(plans, realize):
|
||||
length = float(option.plan.end - option.plan.start)
|
||||
next_length = float(plans[index + 1].end - plans[index + 1].start)
|
||||
shared_passage = bool(plan.passage_id and plan.passage_id == plans[index + 1].passage_id)
|
||||
cost = min((future.score if shared_passage else (option.score * length + future.score * next_length) / (length + next_length)) + transition_cost(option, future) for future in following)
|
||||
ranked.append((cost + incoming, option))
|
||||
for future in following:
|
||||
left, right = revise_pair(option, future, previous) if revise_pair else (option, future)
|
||||
cost = (right.score if shared_passage else (left.score * length + right.score * next_length) / (length + next_length)) + transition_cost(left, right)
|
||||
ranked.append((cost + incoming, left, right))
|
||||
if not ranked:
|
||||
raise failure or generator.solo.realization.SearchExhausted(f'{plan.id}: no feasible phrase transition')
|
||||
_, chosen = min(ranked, key=lambda item: (item[0], tuple((n.pitch, n.start, n.end, n.position) for n in phrase_notes(item[1]))))
|
||||
_, chosen, pending = min(ranked, key=lambda item: (item[0], tuple((n.pitch, n.start, n.end, n.position) for n in phrase_notes(item[1]))))
|
||||
result.append(chosen)
|
||||
notes = phrase_notes(chosen)
|
||||
if chosen.state:
|
||||
|
||||
Reference in New Issue
Block a user