Save
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import fractions
|
||||
import dataclasses
|
||||
|
||||
import generator.solo.policy
|
||||
import generator.solo.types
|
||||
@@ -116,3 +117,75 @@ def value(features):
|
||||
|
||||
def feasible(plan, notes, choices, until):
|
||||
return Evaluation(plan, choices, until).feasible(notes)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class MusicalEvaluation:
|
||||
version: str
|
||||
scope: tuple
|
||||
valid: bool
|
||||
pending: tuple
|
||||
raw: dict
|
||||
denominators: dict
|
||||
weights: dict
|
||||
contributions: dict
|
||||
total: float
|
||||
error: str = ''
|
||||
|
||||
|
||||
MUSICAL_WEIGHTS = {'rhythm':3., 'destination':2., 'harmony':2.,
|
||||
'register':1., 'repetition':1., 'activity':1., 'difficulty':.5}
|
||||
|
||||
|
||||
def musical(plan, draft, notes, incoming, harmony, *, travel=0, weights=None, bar=None):
|
||||
"""The sole final evaluator of the active thematic composer.
|
||||
|
||||
Scores are normalized on complete sounding events, never notation fragments.
|
||||
Contracts are validity requirements, not additional similarity rewards.
|
||||
Retrieval estimates are deliberately not added to this result.
|
||||
"""
|
||||
from generator.solo import drafts, groups, harmonic, phrasing, types
|
||||
notes=types.sounding_notes(notes)
|
||||
bar=getattr(harmony,'bar',fractions.Fraction(4)) if bar is None else bar
|
||||
from generator import vocabulary
|
||||
weights=dict(vocabulary.data('grammar').get('evaluation',{}).get('weights',MUSICAL_WEIGHTS) if weights is None else weights)
|
||||
if set(weights)!=set(MUSICAL_WEIGHTS):raise ValueError('unknown or missing evaluation contribution')
|
||||
checked=harmony.evaluate(notes, incoming.treatments, until=plan.end, complete=True,
|
||||
enclosures=draft.enclosures, expressions=draft.expressions)
|
||||
identity,identity_error=drafts.contract_valid(draft,notes)
|
||||
anchors,anchor_error=harmonic.anchors_valid(draft.anchors,notes,plan.end,complete=True,closure=plan.closure)
|
||||
feasible=Evaluation(plan,harmony.choices).feasible(notes)
|
||||
valid=checked.valid and identity and anchors and feasible
|
||||
error=checked.error or identity_error or anchor_error or ('' if feasible else 'activity intent not fulfilled')
|
||||
span=float(plan.end-plan.start); sounded=sum(float(n.end-n.start) for n in notes)
|
||||
islands=sum(a.end<b.start and b.end<c.start and b.end-b.start<=fractions.Fraction(1,2)
|
||||
for a,b,c in zip(notes,notes[1:],notes[2:]))
|
||||
# Rests at the edges can be entrance/release. Long internal gaps need a
|
||||
# completed gesture, rather than receiving a universal occupancy penalty.
|
||||
settled={a.hold_until for a in draft.anchors}
|
||||
broken=sum(max(0,float(b.start-a.end)-1) for a,b in zip(notes,notes[1:])
|
||||
if a.end not in settled and a.end-a.start<2)
|
||||
entry=max(0,abs(notes[0].pitch.number()-incoming.last.pitch.number())-7)/12 if notes and incoming.last else 0.
|
||||
rhythm=min(1.,islands/max(1,len(notes)-2)+broken/max(1,span)+entry/2)
|
||||
# Explicit source groups retain their imported timing contract. Generated
|
||||
# groups are checked before construction, not retroactively quantized here.
|
||||
destinations=[]
|
||||
for anchor in draft.anchors:
|
||||
approach=[n for n in notes if n.start<anchor.earliest][-3:]
|
||||
if approach:
|
||||
distances=[abs(n.pitch.number()-anchor.pitch.number()) for n in approach]
|
||||
# A broad approach may turn; large last-second jumps are a preference,
|
||||
# not a nearest-note mandate for all intermediate events.
|
||||
destinations.append(min(1,max(0,distances[-1]-5)/12))
|
||||
goal=harmony.profile.reference
|
||||
register=sum(float(n.end-n.start)*min(1,max(0,abs(n.pitch.number()-goal)-9)/12)
|
||||
for n in notes)/max(sounded,.001) if goal is not None else 0.
|
||||
history=phrasing.repetition(phrasing.heard(notes,draft.relationship),incoming.history,bar) if notes else 0.
|
||||
activity=Evaluation(plan,harmony.choices).features(notes)['activity']
|
||||
raw={'rhythm':rhythm,'destination':sum(destinations)/max(1,len(destinations)),
|
||||
'harmony':checked.exposure,'register':register,'repetition':min(1,history),
|
||||
'activity':activity,'difficulty':min(1,travel/max(1,len(notes))/2000)}
|
||||
contributions={name:weights[name]*value for name,value in raw.items()}
|
||||
return MusicalEvaluation('joint-musical-1',(plan.start,plan.end),valid,checked.pending,raw,
|
||||
{'span':span,'sounding':sounded,'attacks':len(notes),'destinations':len(destinations)},weights,
|
||||
contributions,sum(contributions.values()) if valid else float('inf'),error)
|
||||
|
||||
Reference in New Issue
Block a user