"""Independently authored operator, fitting-boundary and cache regressions.""" import copy import dataclasses from fractions import Fraction as F from pathlib import Path import tempfile import unittest import contextlib import io from unittest.mock import patch from generator import resources, vocabulary from generator.solo import expressions, harmonic, operators, selection, types from tests import solo_fixtures as fixture, rhythm_fixtures, scale_fixtures from tests.test_phrase_composer import builder class Operators(unittest.TestCase): def test_supported_sixteenth_tail_can_finish_the_allocation(self): b=builder(('CM7',),count=1) actions=b.operators.at(F(15,4)) self.assertTrue(any(a.kind=='tone' and a.end==4 for a in actions)) # A source/role cutoff does not require a new long cadence. b.start=F(15,4) phrases=b.compose() self.assertEqual(phrases[-1].end,F(4)) self.assertLessEqual(sum(len(p.notes) for p in phrases),1) def test_source_relation_uses_order_and_does_not_reward_subdivision(self): b=builder(('CM7',),count=1) slow=operators.Action(F(),F(4),tuple(fixture.note(i,i+1,p,event_id=str(i)) for i,p in enumerate(('c','d','e','f'))),'scalar') fast=dataclasses.replace(slow,end=F(2),notes=tuple(dataclasses.replace(n,start=n.start/2,end=n.end/2) for n in slow.notes)) self.assertEqual(b.selector.evaluate(slow).raw['prior/source_relation'],b.selector.evaluate(fast).raw['prior/source_relation']) changed=dataclasses.replace(b.analysis,melody=tuple(fixture.note(i,i+1,p,event_id=str(i)) for i,p in enumerate(('e','f','g','a')))) other=selection.Selector(b.harmony,changed,b.selector.center,{}) self.assertNotEqual(b.selector.evaluate(slow).raw.get('prior/source_relation',0),other.evaluate(slow).raw.get('prior/source_relation',0)) @unittest.skipUnless(__import__('importlib.util',fromlist=['find_spec']).find_spec('z3'),'development-only pinned solver not installed') def test_sparse_integer_intervals_are_exact_and_role_separable(self): from audition.structured_learning import solve rows=[{'role':'acoustic_solo','delta':{'duration/1':F(2,3)},'prior':F(1,2),'case':'independent'}, {'role':'electric_solo','delta':{'duration/1_2':F(-1,2)},'prior':F(1,4),'case':'independent'}] result=solve(rows) self.assertEqual(result['weights'],{'acoustic_solo':{'duration/1':-3},'electric_solo':{'duration/1_2':3}}) self.assertEqual(result,solve(list(reversed(rows)))) impossible=[*rows,{'role':'acoustic_solo','delta':{'duration/1':F(-2,3)},'prior':F(1,2),'case':'opposite'}] self.assertEqual(solve(impossible)['status'],'incompatible_with_current_features_and_bounds') def test_full_rollout_uses_the_requested_data_and_role_context(self): import copy from generator import vocabulary from audition.fit_selection import fit resources=copy.deepcopy(vocabulary.current()['data']) resources['roles']['difficulty']['triplet_max_bpm']=123 b=builder(('CM7',),count=1) action=operators.Action(F(),F(4),(fixture.note(0,4,'c'),),'tone') def proposed(at,**kwargs): self.assertEqual(vocabulary.current()['role'],'acoustic_solo') self.assertEqual(vocabulary.data('roles')['difficulty']['triplet_max_bpm'],123) return (action,) if at==0 else () with patch.object(b.operators,'at',side_effect=proposed),contextlib.redirect_stdout(io.StringIO()): result=fit([('synthetic','acoustic_solo',b,(action,))],1,resources=resources) self.assertTrue(result['complete']) def test_complete_training_rounds_accumulate_only_compatible_evidence(self): from audition.train_completed import training_context,previous_constraints data=copy.deepcopy(vocabulary.current()['data']) context=training_context(data) data['roles']['roles']['electric_solo']['selection_weights']={'duration/1':7} data['roles']['provenance']='Explicit development trial' self.assertEqual(context,training_context(data)) cases={'example':{'role':'electric_solo','source_sha256':'source','target_sha256':'example'}} old={'schema':'complete-candidate-training-1','context':context,'training_dependency':cases, 'constraints':[{'case':'example','role':'electric_solo','delta':{'duration/1':'2/3'},'prior':'1/2'}]} row=previous_constraints(old,cases,context)[0] self.assertEqual(row['delta']['duration/1'],F(2,3));self.assertEqual(row['prior'],F(1,2)) changed=copy.deepcopy(cases);changed['example']['target_sha256']='different' with self.assertRaises(ValueError):previous_constraints(old,changed,context) data['roles']['roles']['electric_solo']['register_center']+=12 with self.assertRaises(ValueError):previous_constraints(old,cases,training_context(data)) bad=copy.deepcopy(old);bad['constraints'][0]['delta']={'song/example':1} with self.assertRaises(ValueError):previous_constraints(bad,cases,context) def test_coverage_diagnostic_does_not_accept_a_shortened_target_hold(self): from audition.structured_learning import CoverageTrace from generator.solo.construction import State held=fixture.note(0,2,'e') target=operators.Action(F(),F(2),(held,),'tone') trace=CoverageTrace((target,)) self.assertFalse(trace.matches((dataclasses.replace(held,end=F(1)),),F(1))) self.assertTrue(trace.matches((held,),F(2))) # A computational cut through an already committed hold is different # from selecting a shorter attack and promising to repair it afterward. s=State(action=target,notes=(held,)) trace('frontier',F(1),None,(s,),(s,)) self.assertFalse(trace.records) @unittest.skipUnless(__import__('importlib.util',fromlist=['find_spec']).find_spec('z3'),'development-only pinned solver not installed') def test_shared_fitter_learns_an_independent_synthetic_decision(self): from audition.fit_selection import fit b=builder(('CM7',),count=1) low=operators.Action(F(),F(4),(fixture.note(0,4,'c',event_id='low'),),'tone') high=operators.Action(F(),F(4),(fixture.note(0,4,'e',event_id='high'),),'tone') b.selector.weights={} with patch.object(b.operators,'at',side_effect=lambda at,**kwargs: (low,high) if at==0 else ()),contextlib.redirect_stdout(io.StringIO()): result=fit([('synthetic','acoustic_solo',b,(high,))],10) self.assertTrue(result['complete']) b.selector.weights=result['weights']['acoustic_solo'] self.assertLess(b.selector.evaluate(high).total,b.selector.evaluate(low).total) selection.validate_weights(b.selector.weights) @unittest.skipUnless(__import__('importlib.util',fromlist=['find_spec']).find_spec('z3'),'development-only pinned solver not installed') def test_fitting_normalization_covers_pickup_plus_whole_note(self): from audition.fit_selection import fit b=builder(('CM7',),count=2);b.end=F(17,4) low=operators.Action(F(),b.end,(fixture.note(0,F(1,4),'c',event_id='lead'),fixture.note(F(1,4),b.end,'e',event_id='low')),'anticipation') high=dataclasses.replace(low,notes=(low.notes[0],fixture.note(F(1,4),b.end,'g',event_id='high'))) b.selector.weights={} with patch.object(b.operators,'at',side_effect=lambda at,**kwargs: (low,high) if at==0 else ()),contextlib.redirect_stdout(io.StringIO()): result=fit([('synthetic','acoustic_solo',b,(high,))],10) self.assertTrue(result['complete']) selection.validate_weights(result['weights']['acoustic_solo']) def test_anticipated_reattack_is_not_an_invented_tie(self): b=builder(('C7','FM7'),count=2) actions=b.operators.at(F(7,2)) a=next(a for a in actions if a.kind=='anticipation' and a.source=='early_reattack') self.assertEqual(len(a.notes),2) self.assertEqual(a.notes[0].end,a.notes[1].start) self.assertEqual(a.notes[0].pitch,a.notes[1].pitch) self.assertFalse(a.notes[1].continuation) self.assertTrue(b.harmony.evaluate(a.notes,expressions=a.contracts).valid) self.assertFalse(b.harmony.evaluate(a.notes[:1],expressions=a.contracts).valid) def test_open_motion_and_all_color_octaves_are_constructed(self): b=builder(('C7','FM7'),count=2) actions=b.operators.at(F()) self.assertTrue(any(len(a.notes)==1 and a.notes[0].pitch.number()%12==2 and a.end==F(1,2) for a in actions)) h=builder(('E7','Am7'),count=2) values=h.operators.at(F()) self.assertTrue(any(a.kind=='color' and len(a.notes)==1 and a.notes[0].pitch.number()==66 for a in values)) self.assertTrue(any(a.kind=='rest' for a in values)) def test_anticipation_color_has_actual_future_resolution(self): h=harmonic.Harmony(fixture.choices(('Dm7','G7','CM7'))) held=fixture.note(F(7,2),F(11,2),'c',alter=1,event_id='held') target=fixture.note(F(11,2),7,'d',event_id='target') c=types.ExpressionContract('anticipated','anticipated_color','lydian_color',(held,),target,'G7',boundary=F(4)) self.assertTrue(h.evaluate((held,target),expressions=(c,)).valid) self.assertFalse(h.evaluate((held,),expressions=(c,)).valid) changed=dataclasses.replace(target,pitch=dataclasses.replace(target.pitch,octave=5)) self.assertFalse(h.evaluate((held,changed),expressions=(c,)).valid) self.assertFalse(h.evaluate((held,target)).valid) def test_delayed_approach_is_pending_and_expires(self): h=harmonic.Harmony(fixture.choices(('Dm7',))) approach=fixture.note(0,F(1,2),'c',alter=1,event_id='approach') target=fixture.note(3,F(7,2),'d',event_id='target') c=types.ExpressionContract('delayed','delayed_approach','bounded_semitone_resolution',(approach,),target,'Dm7') r=h.evaluate((approach,),expressions=(c,),until=F(1,2),complete=False) self.assertTrue(r.valid,r.error);self.assertEqual(r.pending,(c,)) self.assertFalse(h.evaluate((approach,),expressions=(c,),until=4,complete=True).valid) self.assertTrue(h.evaluate((approach,target),expressions=(c,)).valid) def test_distinct_pending_targets_are_visible_to_the_selector(self): b=builder(('Dm7',),count=2) actions=[a for a in b.operators.at(F()) if a.kind=='delayed_approach'] first=actions[0] second=next(a for a in actions if a.notes==first.notes and a.contracts[0].target.start!=first.contracts[0].target.start) left,_=b.selector.features(first);right,_=b.selector.features(second) self.assertNotEqual(left,right) selection.validate_weights(dict.fromkeys(left,1)) def test_missed_pending_target_is_rejected_before_expensive_validation(self): from generator.solo.construction import State b=builder(('Dm7',),count=2) approach=fixture.note(0,F(1,2),'c',alter=1,event_id='approach') target=fixture.note(3,4,'d',event_id='target') c=types.ExpressionContract('delayed','delayed_approach','bounded_semitone_resolution',(approach,),target,'Dm7') state=State(pending=(c,)) wrong=operators.Action(F(3),F(4),(fixture.note(3,4,'e',event_id='wrong'),),'tone') with patch.object(b.harmony,'evaluate',side_effect=AssertionError('unnecessary full check')): self.assertIsNone(b.advance(state,None,wrong)) good=dataclasses.replace(wrong,notes=(target,)) self.assertIsNotNone(b.advance(state,None,good)) def test_functional_degree_does_not_depend_on_enharmonic_label(self): h=harmonic.Harmony(fixture.choices(('E7','Am7'))) n=fixture.note(0,2,'g',event_id='sharp9') c=types.ExpressionContract('color','color','dominant_sharp_ninth',(n,),n,'E7') self.assertTrue(h.evaluate((n,),expressions=(c,)).valid) wrong=dataclasses.replace(n,pitch=dataclasses.replace(n.pitch,alter=F(1))) self.assertFalse(h.evaluate((wrong,),expressions=(dataclasses.replace(c,events=(wrong,),target=wrong),)).valid) def test_weight_contributions_and_target_free_features(self): b=builder(count=1) a=next(a for a in b.operators.at(F()) if a.kind=='tone') v=b.selector.evaluate(a) self.assertEqual(v.total,sum(v.contributions.values())) self.assertEqual(v.total,b.selector.cost(a)) key='duration/'+str(a.end-a.start).replace('/','_');b.selector.weights={key:3} changed=b.selector.evaluate(a) self.assertEqual(changed.total,b.selector.cost(a)) self.assertEqual(changed.total-v.total,3*v.raw[key]) with self.assertRaises(ValueError):selection.validate_weights({'song/all_of_me':1}) def test_derivation_cannot_inject_an_illegal_reference_note(self): from audition.fit_selection import derive b=builder(('CM7',),count=1) target={'alignment':{'encoded_offset':'0'},'sounding_events':[{'start':'0','end':'4','sounding_midi':'99','pitch':{},'measure':1}]} path,result=derive(b,target) self.assertFalse(result['complete']);self.assertFalse(path) class CompiledResources(unittest.TestCase): def test_compiled_and_xml_libraries_have_identical_contents(self): original=(scale_fixtures.library(),fixture.library(),rhythm_fixtures.library()) import json encoded=json.loads(json.dumps(resources.encode(original),default=str)) restored=resources.decode(encoded) self.assertEqual(original[0],restored[0]) self.assertEqual(original[1].paths,restored[1].paths) self.assertEqual(original[2].snippets,restored[2].snippets) def test_warm_cache_avoids_xml_import_and_corruption_recompiles(self): original=(scale_fixtures.library(),fixture.library(),rhythm_fixtures.library()) with tempfile.TemporaryDirectory() as root: with patch('generator.resources.scales.load_library',return_value=original[0]) as sc,patch('generator.resources.sequences.load_library',return_value=original[1]),patch('generator.resources.rhythm.load_library',return_value=original[2]): resources._load.cache_clear();resources.load(Path(root),{'fixture':'1'},root) self.assertEqual(sc.call_count,1) resources._load.cache_clear();resources.load(Path(root),{'fixture':'1'},root) self.assertEqual(sc.call_count,1) next((Path(root)/'_resources').glob('*.json')).write_text('{}') resources._load.cache_clear();resources.load(Path(root),{'fixture':'1'},root) self.assertEqual(sc.call_count,2)