Files
product-vamp/tests/test_composer_repair.py
T
khodowany f234dd6d51 Save
2026-10-04 15:51:18 -05:00

178 lines
9.2 KiB
Python

"""Independent musical fixtures; optional target scores are never read."""
import dataclasses
from fractions import Fraction as F
import unittest
from generator.solo import groups, scoring, types, harmonic, themes, realization, profiles
from tests.test_solo_development import note
from tests.test_solo_passages import choices
from tests.test_solo_themes import melody
import tests.solo_fixtures
import tests.rhythm_fixtures
def evaluated(notes,harmony,**kwargs):
motif=types.Motif('authored',(),F(4),(),'test only')
plan=types.PhrasePlan('test',F(),F(4),motif,'continue','thematic_recomposition',None,(41,76),F(1),F(),
(types.ActivitySpan(F(),F(4),'active',F(1),F()),),ending='connected',closure='open')
draft=types.MelodicDraft('test',F(),F(4),tuple(notes),(),(),(),(),None,'continue')
return scoring.musical(plan,draft,notes,types.ContinuationState(''),harmony,**kwargs)
class RhythmGroups(unittest.TestCase):
def test_complete_triplet_can_be_displaced_without_offsetting_binary_tail(self):
pattern=((F(1,2),F(5,6)),(F(5,6),F(7,6)),(F(7,6),F(3,2)),(F(3,2),F(5,2)))
result=groups.describe(pattern)
self.assertEqual(result[0].family,'triplet')
self.assertEqual(result[-1].family,'binary')
self.assertEqual(result[0].displacement,F(1,2))
def test_partial_triplet_does_not_drift_into_binary(self):
self.assertFalse(groups.valid(((F(),F(1,3)),(F(1,3),F(5,6)))))
self.assertFalse(groups.valid(((F(1,3),F(4,3)),)))
self.assertEqual(groups.describe(((F(1,3),F(4,3)),),imported=True)[0].family,'source')
def test_relative_rhythm_identity_does_not_count_absolute_onsets(self):
original=melody(((0,1,'c'),(1,2,'e'),(2,3,'d')))
self.assertEqual(len(set(groups.signature(original))),1)
moved=tuple(dataclasses.replace(n,start=n.start+13,end=n.end+13) for n in original)
self.assertEqual(groups.signature(original),groups.signature(moved))
def test_broadening_never_cuts_inside_tuplet(self):
pattern=((F(),F(1,3)),(F(1,3),F(2,3)),(F(2,3),F(1)))
self.assertEqual(groups.broaden(pattern,F(1,2),F(4)),())
class EvaluationContract(unittest.TestCase):
def test_sum_and_configured_weights_are_authoritative(self):
h=harmonic.Harmony(choices(('C7',)),profiles.Instrument(reference=48))
ns=(note(0,1,'g'),note(2,3,'f'),note(3,4,'e'))
base=evaluated(ns,h,travel=2000)
self.assertTrue(base.valid)
self.assertAlmostEqual(base.total,sum(base.contributions.values()))
for key,value in base.raw.items():
changed=evaluated(ns,h,travel=2000,weights={**base.weights,key:base.weights[key]+1})
self.assertAlmostEqual(changed.total-base.total,value)
self.assertGreater(base.raw['activity'],0)
self.assertGreater(base.raw['harmony'],0)
self.assertGreater(base.raw['register'],0)
def test_ties_are_partition_invariant_and_silence_cannot_win(self):
h=harmonic.Harmony(choices(('C7',)))
whole=(note(0,4,'e',event_id='a'),)
split=(note(0,2,'e',event_id='a'),note(2,4,'e',event_id='a',continuation=True))
self.assertEqual(evaluated(whole,h),evaluated(split,h))
self.assertFalse(evaluated((),h).valid)
def test_computational_end_is_not_a_required_chord_member(self):
h=harmonic.Harmony(choices(('C7',)))
ns=(note(0,1,'g'),note(1,2,'e'),note(2,3,'f'),note(3,4,'a'))
self.assertTrue(evaluated(ns,h).valid)
class PendingTreatments(unittest.TestCase):
def test_complete_route_can_cross_a_search_frontier(self):
from tests.test_independent_generation import TypedExpressionContracts
fixture=TypedExpressionContracts();fixture.setUp();notes,contract=fixture.make()
partial=fixture.h.evaluate(notes[:2],until=F(2),complete=False,expressions=(contract,))
self.assertTrue(partial.valid,partial.error)
self.assertEqual(partial.pending,(contract,))
self.assertFalse(fixture.h.evaluate(notes[:2],until=F(3),expressions=(contract,)).valid)
complete=fixture.h.evaluate(notes,until=F(4),complete=True,expressions=(contract,))
self.assertTrue(complete.valid);self.assertFalse(complete.pending)
class Reachability(unittest.TestCase):
def test_all_development_operations_are_requested(self):
notes=melody(((0,1,'c'),(1,2,'e'),(2,3,'d'),(3,4,'e')))
home=types.RealizedMotif('home','source',notes,F(4),'introduce','contour',signature=notes)
state=types.ContinuationState('',home=home,notes=notes,last=notes[-1])
ops=themes.operation_choices(home,state,F(4),F(4),False)
self.assertTrue({'answer','extend','fragment','continue','return'}.issubset(ops))
h=harmonic.Harmony(choices(('CM7','CM7')))
context=realization.SearchContext()
for op in ops:
candidates=themes.unit_candidates(themes.home_reference(home,op),F(4),F(8),state,h,
tests.solo_fixtures.library(),tests.rhythm_fixtures.library(),'4/4',context,home=home,operation=op)
self.assertTrue(candidates,op)
self.assertGreater(context.diagnostics['operations'][op]['valid'],0)
if __name__=='__main__':unittest.main()
class InputFirewall(unittest.TestCase):
def test_read_write_handles_cannot_bypass_boundary(self):
import tempfile
from pathlib import Path
from generator.input_audit import composition
with tempfile.TemporaryDirectory() as folder:
forbidden=Path(folder)/'target.xml';forbidden.write_text('not musical input')
for mode in ('r','r+'):
with self.assertRaisesRegex(ValueError,'Forbidden composition input'):
with composition(Path('data'),'electric_solo'):
with forbidden.open(mode) as stream:stream.read()
class RetrievalIntegrity(unittest.TestCase):
def test_diversity_optimization_preserves_stable_old_order(self):
from generator.solo.sequences import diverse
values=((3,'b'),(1,'a'),(1,'b'),(1,'a'),(2,'c'),(4,'a'))
def original(items,limit):
families={}
for item in sorted(items,key=lambda v:v[0]):families.setdefault(item[1],[]).append(item)
out=[];i=0
while len(out)<limit:
layer=[g[i] for g in families.values() if len(g)>i]
if not layer:break
out.extend(sorted(layer,key=lambda v:v[0])[:limit-len(out)]);i+=1
return tuple(out)
for limit in range(9):
self.assertEqual(diverse(values,lambda v:v[0],lambda v:v[1],limit),original(values,limit))
def test_seed_cutoff_preserves_different_feature_contracts(self):
from generator.solo import analysis
from tests.test_solo import song
seeds=themes.seeds(analysis.analyze(song(count=12)),F(4),'independent-test')
self.assertGreater(len({s.treatment for s in seeds}),1)
class NonzeroContributions(unittest.TestCase):
def test_rhythm_destination_repetition_are_operational(self):
from generator.solo import phrasing
h=harmonic.Harmony(choices(('C7',)),profiles.Instrument(reference=55))
ns=(note(0,F(1,2),'g'),note(1,F(3,2),'c',octave=5),note(3,4,'e'))
motif=types.Motif('fixture',(),F(4),(),'independently authored')
plan=types.PhrasePlan('fixture',F(),F(4),motif,'continue','thematic_recomposition',None,(41,76),F(1),F(),
(types.ActivitySpan(F(),F(4),'active',F(1),F()),),ending='connected',closure='open')
anchor=types.Anchor('arrival',F(3),F(3),ns[-1].pitch,h.members(F(3),F(4)),(41,76),F(1),'intermediate',h.at(F(3)).chord,hold_until=F(4))
draft=types.MelodicDraft('fixture',F(),F(4),ns,(),(),(),(anchor,),None,'continue')
old=tuple(dataclasses.replace(n,start=n.start-4,end=n.end-4) for n in ns)
incoming=types.ContinuationState('',history=(phrasing.heard(old,'continue'),))
base=scoring.musical(plan,draft,ns,incoming,h)
self.assertTrue(base.valid,base.error)
for key in ('rhythm','destination','repetition'):
self.assertGreater(base.raw[key],0,key)
changed=scoring.musical(plan,draft,ns,incoming,h,weights={**base.weights,key:0})
self.assertAlmostEqual(base.total-changed.total,base.contributions[key])
class PreparedSuspensions(unittest.TestCase):
def test_preparation_and_delayed_resolution_are_required(self):
h=harmonic.Harmony(choices(('Gm7','C7')))
held=note(F(5,2),F(9,2),'f',event_id='held')
target=note(F(9,2),6,'e',event_id='resolution')
contract=types.ExpressionContract('prepared','suspension','prepared_step_down',(held,),target,'C7',boundary=F(4))
self.assertTrue(h.evaluate((held,target),expressions=(contract,)).valid)
self.assertFalse(h.evaluate((held,target)).valid)
changed=dataclasses.replace(held,start=F(15,4))
self.assertFalse(h.evaluate((changed,target),expressions=(dataclasses.replace(contract,events=(changed,)),)).valid)
self.assertFalse(h.evaluate((held,),expressions=(contract,)).valid)
def test_suspension_rule_is_reachable_from_provisional_phrase(self):
from generator.solo import expressions
h=harmonic.Harmony(choices(('Gm7','C7')))
ns=(note(3,4,'f',event_id='held'),note(4,6,'e',event_id='resolution'))
motif=types.Motif('fixture',ns,F(8),(),'independent suspension')
plan=types.PhrasePlan('fixture',F(),F(8),motif,'continue','thematic_recomposition',None,(41,76),F(1),F(),ending='connected',closure='open')
draft=types.MelodicDraft('fixture',F(),F(8),ns,(),(),(),(),None,'continue')
phrase=types.Phrase(plan,(),0,draft=draft)
candidates=[(notes,c) for notes,c in expressions.proposals(phrase,h) if c.kind=='suspension']
self.assertTrue(candidates)
self.assertTrue(any(h.evaluate(notes,expressions=(c,)).valid for notes,c in candidates))