"""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)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))