Fix Playability
ci / checks (push) Failing after 23m26s

This commit is contained in:
khodowany
2026-10-08 19:04:06 -05:00
parent bc7baea7c4
commit 3f63b255ac
23 changed files with 633 additions and 374 deletions
+85
View File
@@ -0,0 +1,85 @@
"""Whole-path position objectives and ties, with independently checked examples."""
import dataclasses
import fractions
import itertools
import unittest
import unittest.mock
import generator.comping
import generator.fretboard
import generator.guitar
import generator.hand_position
import generator.solo.placement
import tests.solo_fixtures
import tests.test_acoustic_voicing
import tests.test_comping_voicings
def position(string, fret):
return generator.fretboard.TabPosition(string, fret, dict(generator.fretboard.STANDARD_TUNING)[string] + fret)
class HandPositionTests(unittest.TestCase):
def test_single_notes_can_share_a_four_fret_hand_region(self):
self.assertEqual(set.intersection(*(set(generator.hand_position.regions((position(1, f),))) for f in range(7, 11))), {7})
self.assertEqual(generator.hand_position.regions((position(1, 1),)), (1,))
self.assertIn(12, generator.hand_position.regions((position(1, 12),)))
self.assertTrue(all(p.fret <= 12 for n in range(41, 89) for p in generator.solo.placement.positions(n)))
def test_chords_share_the_same_window_as_single_notes(self):
self.assertEqual(generator.hand_position.regions((position(5, 7), position(4, 8), position(3, 7), position(2, 8))), (5, 6, 7))
self.assertEqual(generator.hand_position.regions((position(5, 7), position(4, 10))), (7,))
a, b = (position(5, 7),), (position(3, 9),)
motion = generator.hand_position.motion(a, b, 7, 7)
self.assertEqual(motion.shift, 0)
self.assertGreater(motion.ergonomics, 0)
def test_future_peak_can_equalize_histories_at_the_same_endpoint(self):
low, high, join, final = ((position(1, f),) for f in (1, 4, 2, 8))
layers = ((low, high), (join,), (final,))
costs = ({low: generator.comping.VoicingCost(register=10), high: generator.comping.VoicingCost()},
{join: generator.comping.VoicingCost()}, {final: generator.comping.VoicingCost()})
# Isolate peak propagation: a higher earlier peak must remain available
# when a common later maximum exposes its better subordinate tie-break.
with unittest.mock.patch('generator.hand_position.motion', return_value=generator.hand_position.Motion()):
path, cost, hands = generator.comping.select_grips(layers, costs, 'electric', 'peak')
self.assertEqual(path, (high, join, final))
self.assertEqual((cost.travel, cost.peak_position, cost.register), (0, 5, 0))
def test_complete_path_avoids_a_locally_lower_position_trap(self):
low, high, middle, end = ((position(1, f),) for f in (1, 4, 5, 8))
layers = ((low, high), (middle,), (end,))
costs = tuple({v: generator.comping.VoicingCost(register=int(v == high)) for v in layer} for layer in layers)
first, _, _ = generator.comping.select_grips(layers[:1], costs[:1], 'electric', 'lookahead')
path, cost, hands = generator.comping.select_grips(layers, costs, 'electric', 'lookahead')
self.assertEqual(first, (low,))
self.assertEqual(path, (high, middle, end))
self.assertEqual(cost.travel, 1)
self.assertEqual(cost, tests.test_comping_voicings.path_cost(path, costs, 'electric', hands=hands))
def test_lower_travel_beats_easier_finger_changes(self):
notes = tuple(tests.solo_fixtures.note(i, i + 1, pitch, octave=4) for i, pitch in enumerate(('e', 'g', 'f', 'e')))
result = generator.solo.placement.paths(notes, fractions.Fraction(120))[0]
self.assertEqual(result.travel, 0)
self.assertEqual(len(set(result.hands)), 1)
self.assertGreater(result.effort, 0)
def test_solo_matches_exhaustive_pitch_and_hand_paths(self):
notes = (tests.solo_fixtures.note(0, 1, 'e', octave=4), tests.solo_fixtures.note(1, 2, 'g', octave=4), tests.solo_fixtures.note(2, 3, 'f', octave=4))
layers = [tuple((p, h) for p in generator.solo.placement.positions(n.pitch.number()) for h in range(max(1, p.fret - 3), p.fret + 1)) for n in notes]
expected = []
for path in itertools.product(*layers):
effort = sum(200 * (abs(h - k) + abs((p.fret - h) - (q.fret - k)) + abs(p.string_number - q.string_number)) for (p, h), (q, k) in zip(path, path[1:]))
expected.append((sum(abs(h - k) for (_, h), (_, k) in zip(path, path[1:])), max(h for _, h in path), effort))
result = generator.solo.placement.paths(notes, fractions.Fraction(120))[0]
self.assertEqual((result.travel, result.peak, result.effort), min(expected))
self.assertEqual([(n.pitch, n.start, n.end) for n in result.notes], [(n.pitch, n.start, n.end) for n in notes])
report = generator.solo.placement.describe(result.notes, fractions.Fraction(120))
self.assertEqual(fractions.Fraction(report['secondary_effort']), sum(fractions.Fraction(r['secondary_effort']) for r in report['positions']))
def test_tie_split_never_creates_a_shift_or_shorter_travel_time(self):
merged = (tests.solo_fixtures.note(0, 3, 'e', octave=4), tests.solo_fixtures.note(3, 4, 'g', octave=4))
split = (dataclasses.replace(merged[0], end=fractions.Fraction(1)), dataclasses.replace(merged[0], start=fractions.Fraction(1), continuation=True), merged[1])
a, b = (generator.solo.placement.paths(notes, fractions.Fraction(120))[0] for notes in (merged, split))
self.assertEqual((a.travel, a.peak, a.effort), (b.travel, b.peak, b.effort))
self.assertEqual([n.position for n in a.notes], [b.notes[0].position, b.notes[2].position])
self.assertEqual((b.notes[0].position, b.hands[0]), (b.notes[1].position, b.hands[1]))
report = generator.solo.placement.describe(b.notes, fractions.Fraction(120))
self.assertEqual(report['positions'][1]['secondary_effort'], '0')
def test_guitar_cost_composes_peak_with_maximum(self):
first = generator.guitar.VoicingCost(effort=3, peak_position=4, register=fractions.Fraction(1, 2))
second = generator.guitar.VoicingCost(effort=2, peak_position=7, register=fractions.Fraction(1, 3))
result = first.plus(second)
self.assertEqual((result.effort, result.peak_position, result.register), (5, 7, fractions.Fraction(5, 6)))