Files
product-vamp/tests/test_solo_leads.py
T
2026-10-04 09:05:25 -05:00

120 lines
7.6 KiB
Python

"""Lead invention contracts and accepted musical examples, not taste metrics."""
import dataclasses
import fractions
import pathlib
import unittest
import generator.solo.placement
import generator.solo.references as references
import generator.solo.routes as routes
import generator.solo.realization as realization
import generator.solo.types as types
import generator.solo.drafts as drafts
import generator.solo.harmonic as harmonic
import generator.solo.scoring as scoring
import generator.solo.policy as policy
import generator.solo.sequences as sequences
import tests.scale_fixtures
import tests.test_solo_development as fixtures
import tests.test_solo_passages as passages
F = fractions.Fraction
def notes(names):
return tuple(dataclasses.replace(fixtures.note(F(i, 2), F(i + 1, 2), name), event_id=f'n{i}') for i, name in enumerate(names))
class LeadReferenceTests(unittest.TestCase):
def test_authored_sounding_phrase_is_valid_without_audition_files(self):
original=tuple(dataclasses.replace(n,start=n.start*2,end=n.end*2) for n in notes(('c','e','g','e')))
harmony=harmonic.Harmony(passages.choices(('CM7',)))
self.assertTrue(harmony.evaluate(original,complete=True).valid)
self.assertTrue(generator.solo.placement.place(original,F(120)))
def test_feature_selection_finds_the_relationship_not_the_first_three(self):
original = notes(('c', 'd', 'e', 'c', 'g', 'f', 'd'))
feature, = references.identifying_features(original, 'test')
self.assertNotEqual(feature.events[:3], tuple(n.event_id for n in original[:3]))
self.assertEqual(references.feature_error(feature, original), 0)
self.assertGreater(references.feature_error(feature, original[:1]), 0)
def test_single_note_does_not_match_a_complete_tune(self):
phrase = types.Motif('lead', notes(('d', 'g', 'f', 'd')), F(2), ('Gm7',))
self.assertEqual(references.relationship(notes(('c',)), phrase), 1)
self.assertEqual(references.relationship(phrase.notes, phrase), 0)
def test_similarity_is_counted_once(self):
self.assertNotIn('fidelity', dict(policy.DEFAULT.weights))
self.assertEqual(scoring.value({'fidelity': 1, 'development': 0}), 0)
self.assertEqual(scoring.value({'development': 1}), 2)
def test_one_attack_deadband_is_not_a_new_density_target(self):
plan = dataclasses.replace(fixtures.plan(), end=F(4), release=F(), activity=F(1), passages=())
choices = passages.choices(('C7',))
four = notes(('e', 'g', 'e', 'g'))
five = notes(('e', 'g', 'e', 'g', 'e'))
self.assertEqual(scoring.features(plan, four, choices)['activity'], 0)
self.assertEqual(scoring.features(plan, five, choices)['activity'], 0)
self.assertEqual(policy.DEFAULT.lead_activity_range, (1.0, 1.5))
def test_generated_opening_is_editable_until_a_feature_is_committed(self):
original = notes(('c', 'g', 'f', 'e'))
draft = types.MelodicDraft('invented', F(), F(2), original, (), (), (), (), None, 'statement')
changed = (dataclasses.replace(original[0], pitch=fixtures.note(0, 1, 'e').pitch), *original[1:])
self.assertTrue(drafts.contract_valid(draft, changed)[0])
selected = dataclasses.replace(draft, features=references.identifying_features(original, 'selected'))
self.assertFalse(drafts.contract_valid(selected, changed)[0])
def test_ending_activity_only_broadens_the_selected_hold(self):
plan = dataclasses.replace(fixtures.plan(), end=F(16), release=F())
anchor = dataclasses.replace(passages.anchor(), earliest=F(15), latest=F(15), duration=F(1), hold_until=F(16))
chosen = drafts.ending_plan(plan, (anchor,))
arrivals = [p for p in chosen.passages if p.intent == 'sustained_arrival']
self.assertEqual([(p.start, p.end) for p in arrivals], [(15, 16)])
self.assertTrue(all(p.intent == 'active' for p in chosen.passages if p.end <= 15))
def test_zero_passage_origin_retains_the_full_activity_budget(self):
plan = dataclasses.replace(fixtures.plan(), start=F(16), end=F(32), passage_start=F(), passage_end=F(32), activity=F(9, 8), passages=(types.ActivitySpan(F(), F(32), 'active', F(9, 8), F(3, 4)),))
anchor = dataclasses.replace(passages.anchor(), earliest=F(30), latest=F(30), duration=F(2), hold_until=F(32))
chosen = drafts.ending_plan(plan, (anchor,))
self.assertEqual(sum(p.activity * (p.end - p.start) for p in chosen.passages), F(36))
class CompleteRouteTests(unittest.TestCase):
def test_complete_route_reaches_endpoint_without_nearest_note_repairs(self):
harmony = harmonic.Harmony(passages.choices(('C7',)))
library = sequences.Library((sequences.Sequence('descending', (-1, -1)), sequences.Sequence('wrong', (1, 1))))
template = notes(('g', 'c', 'e'))
result = routes.candidates(template, {'n0', 'n2'}, harmony, library, (55, 79))
self.assertEqual(len(result), 1)
self.assertEqual([n.pitch.number() for n in result[0].notes], [67, 65, 64])
self.assertEqual(result[0].sources, ('descending',))
self.assertEqual(result[0].destination, template[-1])
self.assertTrue(harmony.evaluate(result[0].notes).valid)
def test_route_cannot_repair_a_forbidden_fixed_pitch(self):
harmony = harmonic.Harmony(passages.choices(('C7',)))
library = sequences.Library((sequences.Sequence('descending', (-1, -1)),))
self.assertEqual(routes.candidates(notes(('g', 'c', 'e')), {'n1'}, harmony, library, (55, 79)), ())
def test_collection_change_preserves_sounding_coordinate(self):
minor = tests.scale_fixtures.library().scale('harmonic_minor', 1, 'D')
aeolian = tests.scale_fixtures.library().scale('major', 6, 'D')
# C-sharp is absent in Aeolian: the first rising degree is actual D,
# rather than reinterpreting C-sharp's old harmonic-minor index.
self.assertEqual(routes.advance(aeolian, 61, 1)[0].number(), 62)
choices = list(passages.choices(('A7', 'Dm7'), duration=1, scale=minor))
choices[1] = dataclasses.replace(choices[1], scale=aeolian)
harmony = harmonic.Harmony(tuple(choices))
template = (dataclasses.replace(fixtures.note(0, F(1, 2), 'e'), event_id='a'), dataclasses.replace(fixtures.note(F(1, 2), 1, 'c', 1), event_id='b'), dataclasses.replace(fixtures.note(1, F(3, 2), 'd'), event_id='c'))
library = sequences.Library((sequences.Sequence('resolution', (-2, 1)),))
result = routes.candidates(template, {'a', 'c'}, harmony, library, (55, 79))
self.assertTrue(result)
self.assertEqual([n.pitch.number() for n in result[0].notes], [64, 61, 62])
self.assertEqual(len(result[0].spans), 2)
def test_whole_idea_replacement_can_change_an_ordinary_opening(self):
harmony = harmonic.Harmony(passages.choices(('C7', 'C7')))
plan = dataclasses.replace(fixtures.plan(), end=F(8), release=F(), passages=())
def phrase(names, identity):
actual = tuple(dataclasses.replace(n, start=2 * n.start, end=2 * n.end, event_id=identity + n.event_id) for n in notes(names))
group = types.Transformation(identity, '', '', 'statement', F(), F(8), tuple(n.event_id for n in actual), tuple((n.start, n.end) for n in actual), preserves=())
draft = types.MelodicDraft(identity, F(), F(8), actual, (group,), (), (), (), None, 'statement')
return realization.checked_phrase(plan, draft, actual, (), types.ContinuationState(''), harmony, F(120))
first = phrase(('c', 'c', 'c', 'e', 'g', 'g', 'g', 'e'), 'first')
donor = phrase(('e', 'g', 'e', 'c', 'e', 'g', 'f', 'e'), 'donor')
self.assertIsNotNone(first)
self.assertIsNotNone(donor)
revised = realization.replace_idea(first, donor, F(), F(8), types.ContinuationState(''), harmony, F(120))
self.assertIsNotNone(revised)
self.assertEqual([n.pitch for n in realization.emitted_notes(revised)], [n.pitch for n in realization.emitted_notes(donor)])
self.assertEqual(revised.edits[-1].kind, 'replace_idea')