Files
product-vamp/tests/test_phrase_composer.py
T
khodowany 0e68f5364b Cleanup
2026-10-04 18:48:02 -05:00

131 lines
7.6 KiB
Python

"""General, independently authored capabilities; no optional reference inputs."""
import copy,dataclasses,unittest
from fractions import Fraction as F
from unittest.mock import patch
from generator import vocabulary,model
from generator.scales import selection,timeline
from generator.solo import analysis,construction,harmonic,profiles,types,scoring,composer,routes,placement
from tests import solo_fixtures as fixtures,rhythm_fixtures,scale_fixtures
def builder(symbols=('C7','FM7','Dm7','G7'),seed='independent',count=8):
source=fixtures.song(count=count,chords=symbols);a=analysis.analyze(source);ms=source['staves'][0]['measures']
end=sum(model.fraction(m['duration']) for m in ms)
cs=selection.select(timeline.read(source['staves'][0]),timeline.Solo(F(),end,'test'),scale_fixtures.library())
return construction.Builder(a,ms,harmonic.Harmony(cs,profiles.Instrument(reference=60)),fixtures.library(),rhythm_fixtures.library(),end,seed)
class PhraseConstruction(unittest.TestCase):
def test_complete_candidates_have_varied_functions_and_joint_expression(self):
b=builder();values=[b.propose(F(),(),i) for i in range(64)];values=[p for p in values if p]
self.assertTrue(values)
self.assertIn('open',{p.ending for p in values})
self.assertTrue(any(p.destination and p.destination.duration>=2 for p in values))
self.assertTrue(any(p.expressions for p in values))
for p in values:
self.assertTrue(b.harmony.evaluate(p.notes,until=p.end,complete=True,expressions=p.expressions).valid)
self.assertEqual(p.score,p.evaluation.total)
self.assertTrue(all(p.start<=n.start<n.end<=p.end for n in p.notes))
def test_all_operations_are_eligible_and_construct_complete_music(self):
b=builder(count=12);first=b.candidates(F(),())[0]
history=(first,dataclasses.replace(first,id='second'))
self.assertEqual(set(b.actions(history)),set(vocabulary.data('solo')['actions'])-{'head'})
history=(*history,dataclasses.replace(first,id='third'))
self.assertEqual(set(b.actions(history)),set(vocabulary.data('solo')['actions']))
# Request each operation through the same active constructor in eligible contexts.
for action in b.actions(history):
with self.subTest(action=action),patch.object(b,'actions',return_value=[action]):
candidates=[b.propose(F(16),history,i) for i in range(32)]
self.assertTrue(any(p and p.purpose==action for p in candidates))
def test_head_is_optional_not_required_rhythm(self):
b=builder();b.analysis=dataclasses.replace(b.analysis,melody=())
values=b.candidates(F(),())
self.assertTrue(values)
self.assertTrue(all(p.purpose!='head' for p in values))
def test_recomposed_opening_is_not_source_hold(self):
source=fixtures.song(count=4,chords=('CM7',))
n=source['staves'][0]['measures'][0]['voices'][0]['notes'][0]
source['staves'][0]['measures'][0]['voices'][0]['notes']=[{**n,'duration':model.rational(4),'pitches':[{**n['pitches'][0],'duration':model.rational(4)}]}]
result=fixtures.compose(source)
self.assertTrue(result.notes());self.assertLess(result.notes()[0].end-result.notes()[0].start,4)
def test_disabled_expressions_are_strict_and_reproducible(self):
b=builder();b.enabled=False
ps=b.compose();ns=tuple(n for p in ps for n in p.notes)
self.assertFalse(any(p.expressions for p in ps))
self.assertTrue(all(n.pitch.number()%12 in types.permitted(b.harmony.choices,n.start,n.end) for n in ns))
self.assertEqual(ps,b.compose())
def test_role_domains_and_complete_tied_position(self):
for role in ('acoustic','electric'):
p=profiles.for_part(role)
self.assertEqual(p.strings,(1,2,3,4,5,6))
for midi in range(41,77):self.assertTrue(placement.positions(midi,p))
a=fixtures.note(0,2,'e',event_id='held');ns=(a,dataclasses.replace(a,start=F(2),end=F(4),continuation=True))
positioned=placement.place(ns,F(120))[1]
self.assertEqual(positioned[0].position,positioned[1].position)
self.assertEqual(len(types.sounding_notes(positioned)),1)
def test_routes_keep_actual_pitches_across_collection_change(self):
b=builder(('C7','DmM7'),count=2)
ns=(fixtures.note(3,F(7,2),'g'),fixtures.note(F(7,2),4,'a'),fixtures.note(4,F(9,2),'c'),fixtures.note(F(9,2),5,'d'))
values=b.routes.realize(ns,{0,3},'scalar','up','crossing',64)
self.assertTrue(values)
for route in values:
self.assertEqual(route[0].pitch,ns[0].pitch);self.assertEqual(route[-1].pitch,ns[-1].pitch)
self.assertTrue(all(n.pitch.number()%12 in types.permitted(b.harmony.choices,n.start,n.end) for n in route))
def test_no_internal_chordal_endpoint_requirement(self):
b=builder(('C7',),count=2)
ns=(fixtures.note(0,F(1,2),'e'),fixtures.note(F(1,2),1,'f'))
paths=b.routes.realize(ns,{0,1},'scalar','up','open',64)
self.assertTrue(paths)
self.assertTrue(b.harmony.evaluate(paths[0]).valid)
self.assertNotIn(paths[0][-1].pitch.number()%12,b.harmony.at(F(1,2)).roles.basic)
def test_meaningful_incoming_state_changes_continuations(self):
b=builder();history=(b.candidates(F(),())[0],)
a=b.candidates(history[-1].end,history)
old=history[0];last=old.notes[-1]
changed=dataclasses.replace(old,id='changed',notes=(*old.notes[:-1],dataclasses.replace(last,pitch=dataclasses.replace(last.pitch,octave=last.pitch.octave+1))))
c=b.candidates(changed.end,(changed,))
self.assertNotEqual(tuple(p.notes for p in a),tuple(p.notes for p in c))
def test_no_short_accidental_handoff_tail(self):
b=builder(count=4);ps=b.compose()
self.assertEqual(ps[-1].end,b.end)
if ps[-1].destination:self.assertGreaterEqual(ps[-1].destination.duration,2)
def test_cache_restores_exact_music_without_source_ancestry_growth(self):
b=builder(count=4);cold=b.candidates(F(),());warm=b.candidates(F(),())
self.assertEqual(cold,warm)
self.assertTrue(all(not n.sources for p in cold for n in p.notes))
class EvaluationContract(unittest.TestCase):
def phrase(self,ns,**kwargs):return types.Phrase('test',F(),F(8),tuple(ns),'scalar','up','open',**kwargs)
def test_configured_terms_are_authoritative_and_nonzero(self):
ns=(fixtures.note(0,1,'c',octave=3),fixtures.note(1,2,'e',octave=4),fixtures.note(2,4,'c',octave=4))
p=self.phrase(ns);incoming=fixtures.note(-1,0,'c',octave=5)
base=scoring.evaluate(p,incoming,(p,),40)
self.assertEqual(base.total,sum(base.contributions.values()))
for name,value in base.raw.items():
self.assertGreater(value,0,name)
altered=scoring.evaluate(p,incoming,(p,),40,weights={**base.weights,name:base.weights[name]+1})
self.assertAlmostEqual(altered.total-base.total,value)
def test_silence_is_explicit_and_tie_partition_is_invariant(self):
a=fixtures.note(0,4,'e',event_id='a')
split=(dataclasses.replace(a,end=F(2)),dataclasses.replace(a,start=F(2),continuation=True))
self.assertEqual(scoring.evaluate(self.phrase((a,)),None,(),60),scoring.evaluate(self.phrase(split),None,(),60))
self.assertFalse(scoring.evaluate(self.phrase(()),None,(),60).valid)
self.assertTrue(scoring.evaluate(dataclasses.replace(self.phrase(()),purpose='space'),None,(),60).valid)
def test_target_touch_and_repeated_attacks_do_not_fulfill_hold(self):
a=fixtures.note(2,4,'e',event_id='arrival')
anchor=types.Anchor('held',F(2),F(2),a.pitch,frozenset({4}),(41,76),F(2),'terminal','C7',hold_until=F(4))
ns=(dataclasses.replace(a,end=F(3)),dataclasses.replace(a,start=F(3),event_id='again'))
self.assertFalse(harmonic.anchors_valid((anchor,),ns,F(4),complete=True,closure='closed')[0])
self.assertTrue(harmonic.anchors_valid((anchor,),(a,),F(4),complete=True,closure='closed')[0])
if __name__=='__main__':unittest.main()