Files
product-vamp/tests/test_joint_actions.py
T
khodowany 654299decd Cleanup
2026-10-05 08:51:03 -05:00

234 lines
14 KiB
Python

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