FAILURE MAP
← Case archive

FA-80921 / Music interval and transposition theory / Open access

Spelled transposition by named interval: octave does not carry across the B-C letter boundary · case 01

B3 up a minor second returns None instead of C4, because the octave never carries past the letter B.

Verified by executionVariant 1 · 8 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

The new octave is computed from compound size alone and ignores letter wrap-around past B or below C.

VERIFIED REPAIR

Restore the octave from letter position step so that it reads `new_octave = idx // 7`.

Unsuccessful approach: Taking the octave from the sounding pitch breaks spellings like B#3 and Cb4 whose letters sit in a different octave.

Case contract

Input [note, interval, direction]. note: letter, # or b accidentals, nonnegative octave. interval: quality letter (P, M, m, A, d) and generic number >= 1 with perfect-class numbers 1, 4, 5 (compounds reduce by 7). Direction up or down moves the letter by number-1 steps and the pitch by the interval size; the result is spelled with the moved letter, octave taken from the letter position. Results needing more than two accidentals, or a negative octave, return None.

Why this case matters

Pitch spelling and interval arithmetic decide the notes that notation, transposition and analysis tools display or play.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(x):
    base = {'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11}
    if not isinstance(x, list) or len(x) != 3:
        return None
    note, ivl, direction = x
    if direction not in ('up', 'down'):
        return None
    if not isinstance(note, str) or len(note) < 2 or note[0] not in base:
        return None
    i = 1
    acc = 0
    while i < len(note) and note[i] in '#b':
        acc += 1 if note[i] == '#' else -1
        i += 1
    if not note[i:].isdigit():
        return None
    octave = int(note[i:])
    if not isinstance(ivl, str) or len(ivl) < 2 or not ivl[1:].isdigit():
        return None
    quality = ivl[0]
    number = int(ivl[1:])
    if number < 1:
        return None
    simple = (number - 1) % 7 + 1
    perfect = simple in (1, 4, 5)
    offsets = {'P': 0, 'A': 1, 'd': -1} if perfect else {'M': 0, 'm': -1, 'A': 1, 'd': -2}
    if quality not in offsets:
        return None
    size = [0, 2, 4, 5, 7, 9, 11][simple - 1] + 12 * ((number - 1) // 7) + offsets[quality]
    sign = 1 if direction == 'up' else -1
    idx = 'CDEFGAB'.index(note[0]) + 7 * octave + sign * (number - 1)
    pitch = 12 * octave + base[note[0]] + acc + sign * size
    letter = 'CDEFGAB'[idx % 7]
    new_octave = octave + sign * ((number - 1) // 7)
    new_acc = pitch - (12 * new_octave + base[letter])
    if abs(new_acc) > 2 or new_octave < 0:
        return None
    return letter + ('#' * new_acc if new_acc > 0 else 'b' * -new_acc) + str(new_octave)
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[(['C4', 'M3', 'up'], 'E4'), (['C4', 'M3', 'down'], 'Ab3'), (['C4', 'm2', 'down'], 'B3'), (['E4', 'A4', 'up'], 'A#4'), (['F#4', 'A4', 'up'], 'B#4'), (['C4', 'd5', 'up'], 'Gb4'), (['C4', 'd7', 'up'], 'Bbb4'), (['F4', 'A4', 'down'], 'Cb4')], [(['B3', 'm2', 'up'], 'C4'), (['C4', 'd7', 'up'], 'Bbb4'), (['C4', 'd3', 'up'], 'Ebb4'), (['Db4', 'd3', 'up'], 'Fbb4'), (['Cb4', 'd3', 'up'], None), (['C4', 'M10', 'down'], 'Ab2'), (['C4', 'd1', 'up'], 'Cb4'), (['Bb4', 'd2', 'up'], 'Cbb5')], [(['C4', 'm2', 'down'], 'B3'), (['F#4', 'A4', 'up'], 'B#4'), (['Cb4', 'd3', 'up'], None), (['C4', 'P8', 'up'], 'C5'), (['C4', 'M9', 'up'], 'D5'), (['G4', 'P5', 'up'], 'D5'), (['E#4', 'A6', 'up'], None), (['C4', 'A1', 'down'], 'Cb4')], [(['C4', 'M10', 'down'], 'Ab2'), (['A4', 'M6', 'up'], 'F#5'), (['E#4', 'A6', 'up'], None), (['C0', 'm2', 'down'], None), (['C4', 'X3', 'up'], None), (['C4', 'M4', 'up'], None), (['C4', 'd1', 'up'], 'Cb4'), (['F4', 'A4', 'down'], 'Cb4')], [(['G4', 'P5', 'up'], 'D5'), (['C4', 'M4', 'up'], None), (['C4', 'P3', 'up'], None), (['C4', 'M3', 'sideways'], None), (['Bb3', 'A2', 'up'], 'C#4'), (['C4', 'A1', 'down'], 'Cb4'), (['C4', 'd1', 'down'], 'C#4'), (['Bb4', 'd2', 'up'], 'Cbb5')]]
for i, (args, expected) in enumerate(fixtures[N-1]):
    check("oracle %d" % i, solve(args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
oracle 0E4E4Passed
oracle 1NoneAb3Failed
oracle 2NoneB3Failed
oracle 3A#4A#4Passed
oracle 4B#4B#4Passed
oracle 5Gb4Gb4Passed
oracle 6Bbb4Bbb4Passed
oracle 7Cb4Cb4Passed

SHA-256 / def81a166838a31e2a91187c1df9e8530f037d06b850afb3bc76cef8ab1a4ecb

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(x):
    base = {'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11}
    if not isinstance(x, list) or len(x) != 3:
        return None
    note, ivl, direction = x
    if direction not in ('up', 'down'):
        return None
    if not isinstance(note, str) or len(note) < 2 or note[0] not in base:
        return None
    i = 1
    acc = 0
    while i < len(note) and note[i] in '#b':
        acc += 1 if note[i] == '#' else -1
        i += 1
    if not note[i:].isdigit():
        return None
    octave = int(note[i:])
    if not isinstance(ivl, str) or len(ivl) < 2 or not ivl[1:].isdigit():
        return None
    quality = ivl[0]
    number = int(ivl[1:])
    if number < 1:
        return None
    simple = (number - 1) % 7 + 1
    perfect = simple in (1, 4, 5)
    offsets = {'P': 0, 'A': 1, 'd': -1} if perfect else {'M': 0, 'm': -1, 'A': 1, 'd': -2}
    if quality not in offsets:
        return None
    size = [0, 2, 4, 5, 7, 9, 11][simple - 1] + 12 * ((number - 1) // 7) + offsets[quality]
    sign = 1 if direction == 'up' else -1
    idx = 'CDEFGAB'.index(note[0]) + 7 * octave + sign * (number - 1)
    pitch = 12 * octave + base[note[0]] + acc + sign * size
    letter = 'CDEFGAB'[idx % 7]
    new_octave = pitch // 12
    new_acc = pitch - (12 * new_octave + base[letter])
    if abs(new_acc) > 2 or new_octave < 0:
        return None
    return letter + ('#' * new_acc if new_acc > 0 else 'b' * -new_acc) + str(new_octave)
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[(['C4', 'M3', 'up'], 'E4'), (['C4', 'M3', 'down'], 'Ab3'), (['C4', 'm2', 'down'], 'B3'), (['E4', 'A4', 'up'], 'A#4'), (['F#4', 'A4', 'up'], 'B#4'), (['C4', 'd5', 'up'], 'Gb4'), (['C4', 'd7', 'up'], 'Bbb4'), (['F4', 'A4', 'down'], 'Cb4')], [(['B3', 'm2', 'up'], 'C4'), (['C4', 'd7', 'up'], 'Bbb4'), (['C4', 'd3', 'up'], 'Ebb4'), (['Db4', 'd3', 'up'], 'Fbb4'), (['Cb4', 'd3', 'up'], None), (['C4', 'M10', 'down'], 'Ab2'), (['C4', 'd1', 'up'], 'Cb4'), (['Bb4', 'd2', 'up'], 'Cbb5')], [(['C4', 'm2', 'down'], 'B3'), (['F#4', 'A4', 'up'], 'B#4'), (['Cb4', 'd3', 'up'], None), (['C4', 'P8', 'up'], 'C5'), (['C4', 'M9', 'up'], 'D5'), (['G4', 'P5', 'up'], 'D5'), (['E#4', 'A6', 'up'], None), (['C4', 'A1', 'down'], 'Cb4')], [(['C4', 'M10', 'down'], 'Ab2'), (['A4', 'M6', 'up'], 'F#5'), (['E#4', 'A6', 'up'], None), (['C0', 'm2', 'down'], None), (['C4', 'X3', 'up'], None), (['C4', 'M4', 'up'], None), (['C4', 'd1', 'up'], 'Cb4'), (['F4', 'A4', 'down'], 'Cb4')], [(['G4', 'P5', 'up'], 'D5'), (['C4', 'M4', 'up'], None), (['C4', 'P3', 'up'], None), (['C4', 'M3', 'sideways'], None), (['Bb3', 'A2', 'up'], 'C#4'), (['C4', 'A1', 'down'], 'Cb4'), (['C4', 'd1', 'down'], 'C#4'), (['Bb4', 'd2', 'up'], 'Cbb5')]]
for i, (args, expected) in enumerate(fixtures[N-1]):
    check("oracle %d" % i, solve(args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
oracle 0E4E4Passed
oracle 1Ab3Ab3Passed
oracle 2B3B3Passed
oracle 3A#4A#4Passed
oracle 4NoneB#4Failed
oracle 5Gb4Gb4Passed
oracle 6Bbb4Bbb4Passed
oracle 7NoneCb4Failed

SHA-256 / 1f3a8e4475cec396a9ee0f93419eec1e28f0fc9b0fcec4cf8d1d033fcbe739a9

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(x):
    base = {'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11}
    if not isinstance(x, list) or len(x) != 3:
        return None
    note, ivl, direction = x
    if direction not in ('up', 'down'):
        return None
    if not isinstance(note, str) or len(note) < 2 or note[0] not in base:
        return None
    i = 1
    acc = 0
    while i < len(note) and note[i] in '#b':
        acc += 1 if note[i] == '#' else -1
        i += 1
    if not note[i:].isdigit():
        return None
    octave = int(note[i:])
    if not isinstance(ivl, str) or len(ivl) < 2 or not ivl[1:].isdigit():
        return None
    quality = ivl[0]
    number = int(ivl[1:])
    if number < 1:
        return None
    simple = (number - 1) % 7 + 1
    perfect = simple in (1, 4, 5)
    offsets = {'P': 0, 'A': 1, 'd': -1} if perfect else {'M': 0, 'm': -1, 'A': 1, 'd': -2}
    if quality not in offsets:
        return None
    size = [0, 2, 4, 5, 7, 9, 11][simple - 1] + 12 * ((number - 1) // 7) + offsets[quality]
    sign = 1 if direction == 'up' else -1
    idx = 'CDEFGAB'.index(note[0]) + 7 * octave + sign * (number - 1)
    pitch = 12 * octave + base[note[0]] + acc + sign * size
    letter = 'CDEFGAB'[idx % 7]
    new_octave = idx // 7
    new_acc = pitch - (12 * new_octave + base[letter])
    if abs(new_acc) > 2 or new_octave < 0:
        return None
    return letter + ('#' * new_acc if new_acc > 0 else 'b' * -new_acc) + str(new_octave)
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[(['C4', 'M3', 'up'], 'E4'), (['C4', 'M3', 'down'], 'Ab3'), (['C4', 'm2', 'down'], 'B3'), (['E4', 'A4', 'up'], 'A#4'), (['F#4', 'A4', 'up'], 'B#4'), (['C4', 'd5', 'up'], 'Gb4'), (['C4', 'd7', 'up'], 'Bbb4'), (['F4', 'A4', 'down'], 'Cb4')], [(['B3', 'm2', 'up'], 'C4'), (['C4', 'd7', 'up'], 'Bbb4'), (['C4', 'd3', 'up'], 'Ebb4'), (['Db4', 'd3', 'up'], 'Fbb4'), (['Cb4', 'd3', 'up'], None), (['C4', 'M10', 'down'], 'Ab2'), (['C4', 'd1', 'up'], 'Cb4'), (['Bb4', 'd2', 'up'], 'Cbb5')], [(['C4', 'm2', 'down'], 'B3'), (['F#4', 'A4', 'up'], 'B#4'), (['Cb4', 'd3', 'up'], None), (['C4', 'P8', 'up'], 'C5'), (['C4', 'M9', 'up'], 'D5'), (['G4', 'P5', 'up'], 'D5'), (['E#4', 'A6', 'up'], None), (['C4', 'A1', 'down'], 'Cb4')], [(['C4', 'M10', 'down'], 'Ab2'), (['A4', 'M6', 'up'], 'F#5'), (['E#4', 'A6', 'up'], None), (['C0', 'm2', 'down'], None), (['C4', 'X3', 'up'], None), (['C4', 'M4', 'up'], None), (['C4', 'd1', 'up'], 'Cb4'), (['F4', 'A4', 'down'], 'Cb4')], [(['G4', 'P5', 'up'], 'D5'), (['C4', 'M4', 'up'], None), (['C4', 'P3', 'up'], None), (['C4', 'M3', 'sideways'], None), (['Bb3', 'A2', 'up'], 'C#4'), (['C4', 'A1', 'down'], 'Cb4'), (['C4', 'd1', 'down'], 'C#4'), (['Bb4', 'd2', 'up'], 'Cbb5')]]
for i, (args, expected) in enumerate(fixtures[N-1]):
    check("oracle %d" % i, solve(args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
oracle 0E4E4Passed
oracle 1Ab3Ab3Passed
oracle 2B3B3Passed
oracle 3A#4A#4Passed
oracle 4B#4B#4Passed
oracle 5Gb4Gb4Passed
oracle 6Bbb4Bbb4Passed
oracle 7Cb4Cb4Passed

SHA-256 / 77eb68f6bfa2aa6a1b638d1cd65833400cc19946534450fac9dc5aadd8d6988c

Verification & scope

A deterministic bounded teaching model with a stipulated toy contract; it is not a complete music notation or theory engine. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.

Observations recorded using Python 3.12.14 at 2026-09-29T14:49:58.239137+00:00.

Case digest / 067f35f23fd389f0e1f532de3b794af26e10af2030118819f04f9efebfc55e39