"""Source influence and realized-theme continuity are different contracts.""" import dataclasses from fractions import Fraction as F import unittest from generator.solo import analysis, composer, drafts, harmonic, leads, references, realization, similarity, themes, types import tests.test_solo as fixture import tests.test_solo_development as notes_fixture import tests.test_solo_passages as harmony_fixture import tests.scale_fixtures import tests.solo_fixtures import tests.rhythm_fixtures def melody(values): return tuple(dataclasses.replace(notes_fixture.note(a,b,p),event_id=str(i),sources=(str(i),)) for i,(a,b,p) in enumerate(values)) class ThemeContracts(unittest.TestCase): def test_selected_rhythm_does_not_also_require_source_pitches(self): source=melody(((0,1,'c'),(1,2,'d'),(2,3,'e'))) ref=types.SourceReference('source','phrase',F(),F(3),source,'rhythmic_seed') actual=melody(((8,9,'g'),(9,10,'e'),(10,11,'c'))) self.assertEqual(references.feature_error(themes.feature(ref,actual),actual),0) contour=dataclasses.replace(ref,treatment='contour_seed') self.assertGreater(references.feature_error(themes.feature(contour,actual),actual),0) def test_a_single_event_is_not_evidence_of_a_multinote_feature(self): source=melody(((0,1,'c'),(1,2,'d'),(2,3,'e'))) ref=types.SourceReference('source','phrase',F(),F(3),source,'rhythmic_seed') contract=themes.feature(ref,source) self.assertEqual(references.feature_error(contract,source[:1]),1) self.assertEqual(references.feature_error(themes.feature(ref,source[:1]),source[:1]),1) def test_copy_comparison_cannot_be_diluted_by_octaves_runs_or_reattacks(self): source=melody(((0,2,'c'),(2,4,'e'),(4,6,'g'),(6,8,'c'))) shifted=leads.shift(source,1) self.assertTrue(similarity.near_copy(shifted,source,F(),F(8))['near_copy']) repeated=tuple(part for n in shifted for part in (dataclasses.replace(n,end=n.start+1),dataclasses.replace(n,start=n.start+1,event_id=n.event_id+'r'))) self.assertTrue(similarity.near_copy(repeated,source,F(),F(8))['near_copy']) self.assertEqual(similarity.compare(repeated,source)['octave_insensitive'],1) shortened=tuple(dataclasses.replace(n,end=n.end-F(1,2)) for n in shifted) self.assertEqual(similarity.compare(shortened,source)['octave_insensitive'],.75) relocated=tuple(dataclasses.replace(n,start=n.start+8,end=n.end+8) for n in shifted) self.assertTrue(similarity.near_copy(relocated,source,F(8),F(16))['near_copy']) def test_tied_partition_does_not_change_similarity(self): source=melody(((0,2,'c'),(2,4,'e'),(4,6,'g'),(6,8,'c'))) split=tuple(piece for n in source for piece in (dataclasses.replace(n,end=n.start+1),dataclasses.replace(n,start=n.start+1,continuation=True))) self.assertEqual(similarity.compare(source,source),similarity.compare(split,source)) def test_downward_cross_collection_steps_preserve_the_actual_target(self): from generator.solo import routes scale=tests.scale_fixtures.library().scale('major',6,'D') harmony=harmonic.Harmony(harmony_fixture.choices(('Dm7',),scale=scale)) first=dataclasses.replace(notes_fixture.note(0,1,'c'),pitch=types.Pitch('c',F(1),4)) last=dataclasses.replace(notes_fixture.note(1,2,'b'),pitch=types.Pitch('b',F(-1),3)) step,=themes.relative_steps((first,last),harmony) self.assertEqual(step,-2) self.assertEqual(routes.advance(scale,first.pitch.number(),step)[0].number(),last.pitch.number()) def test_two_note_route_uses_exact_scale_displacement(self): from generator.solo import routes harmony=harmonic.Harmony(harmony_fixture.choices(('C7',))) notes=melody(((0,1,'g'),(1,2,'e'))) options=routes.candidates(notes,{'1'},harmony,tests.solo_fixtures.library(),(41,76)) self.assertTrue(options) self.assertTrue(all(r.notes[-1].pitch==notes[-1].pitch for r in options)) def test_elaboration_may_continue_its_entry_instead_of_reattacking(self): notes=melody(((0,3,'g'),(3,4,'e'))) source=types.SourcePhrase('held_entry',F(),F(4),notes,(),'') harmony=harmonic.Harmony(harmony_fixture.choices(('C7',))) options=leads.elaborate(notes,(),harmony,tests.solo_fixtures.library(),tests.rhythm_fixtures.library(),'4/4',source,sustain_entry=True) self.assertTrue(options) for actual,edit,route in options: self.assertEqual(actual[0].start,0) self.assertGreater(actual[0].end,edit.start) self.assertEqual(actual[:len(route.notes)],route.notes) self.assertTrue(harmony.evaluate(actual,complete=True).valid) class ThemeProduction(unittest.TestCase): @classmethod def setUpClass(cls): cls.document=fixture.song(count=8,chords=('CM7',)) cls.analysis=analysis.analyze(cls.document) cls.context=realization.SearchContext() cls.kw=dict(scales=tests.scale_fixtures.library(),sequences=tests.solo_fixtures.library(),rhythms=tests.rhythm_fixtures.library(),context=cls.context) cls.solo=composer.compose(cls.analysis,cls.document['staves'][0]['measures'],'acoustic',F(32),**cls.kw) def test_one_heard_theme_develops_across_source_boundaries(self): self.assertEqual(self.solo.diagnostics['construction'],'thematic_recomposition') self.assertEqual(len({p.state.home.id for p in self.solo.phrases}),1) for phrase in self.solo.phrases: self.assertTrue(drafts.contract_valid(phrase.draft,composer.phrase_notes(phrase))[0]) self.assertFalse(similarity.near_copy(phrase.refined.notes,themes.source_notes(self.analysis),phrase.plan.start,phrase.plan.end)['near_copy']) self.assertEqual(self.solo.phrases[0].plan.closure,'closed') self.assertTrue(all(a.duration>=2 for a in self.solo.phrases[0].anchors)) self.assertEqual(self.solo.phrases[-1].anchors[-1].role,'terminal') self.assertTrue(any(t.operation in ('develop_contour','develop_rhythm','continue','return') for p in self.solo.phrases for t in p.draft.transformations)) def test_library_routes_match_the_actual_complete_theme(self): self.assertTrue(any(p.routes for p in self.solo.phrases)) for phrase in self.solo.phrases: heard={n.event_id:n for n in composer.phrase_notes(phrase)} for route in phrase.routes: for note in route.notes: actual=heard[note.event_id] self.assertEqual((note.start,note.end,note.pitch),(actual.start,actual.end,actual.pitch)) self.assertEqual(route.notes[-1].pitch,route.destination.pitch) def test_report_can_compare_a_source_hold_cut_by_new_phrase_boundaries(self): from generator.solo import report phrases=[] for i,p in enumerate(self.solo.phrases): held=notes_fixture.note(p.plan.start,p.plan.end,'c',event_id='source_hold',continuation=i>0) phrases.append(dataclasses.replace(p,initial=dataclasses.replace(p.initial,notes=(held,)))) solo=dataclasses.replace(self.solo,phrases=tuple(phrases)) detail=report.structure(solo) self.assertEqual(len(detail['phrases']),len(phrases)) self.assertTrue(phrases[-1].initial.notes[0].continuation) def test_incoming_pitch_changes_concrete_continuation_pool(self): harmony=harmonic.Harmony(self.solo.choices) home=self.solo.phrases[0].state.home ref=themes.home_reference(home,'answer') context=realization.SearchContext() outputs=[] for octave in (3,4): prior=dataclasses.replace(notes_fixture.note(15,16,'e'),pitch=types.Pitch('e',F(),octave)) state=types.ContinuationState('prior',last=prior,home=home) options=themes.unit_candidates(ref,F(16),F(24),state,harmony,self.kw['sequences'],self.kw['rhythms'],'4/4',context,home=home,operation='answer') self.assertTrue(options) outputs.append({u.notes[0].pitch.number() for u in options}) self.assertNotEqual(*outputs) def test_deterministic_cache_and_actual_source_input(self): repeated=composer.compose(self.analysis,self.document['staves'][0]['measures'],'acoustic',F(32),**self.kw) self.assertEqual(self.solo.notes(),repeated.notes()) changed=dataclasses.replace(self.analysis,melody=tuple(dataclasses.replace(n,pitch=dataclasses.replace(n.pitch,letter='g')) for n in self.analysis.melody)) other=composer.compose(changed,self.document['staves'][0]['measures'],'acoustic',F(32),**self.kw) self.assertNotEqual(leads.signature(self.solo.notes()),leads.signature(other.notes())) if __name__=='__main__':unittest.main()