FAILURE MAP
← Case archive

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

Parallel fifths and octaves between two voices: fifth to octave counted as parallel · case 01

Similar motion from a fifth to an octave (G4/C4 to D5/D4) is reported as a parallel perfect interval.

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

ROOT CAUSE

The check only requires both intervals to be perfect, not the same perfect class.

VERIFIED REPAIR

Restore the same class requirement step so that it reads `if a != b or a not in (0, 7):`.

Unsuccessful approach: Allowing any interval that arrives on an octave still reports fifth-to-octave motion.

Case contract

Input [upper, lower], equal-length lists of MIDI pitches. For each adjacent pair of positions i, i+1 report [i, "P5"] or [i, "P8"] when the interval (upper - lower) mod 12 is 7 or 0 at both positions (the same class), both voices move (no repeated note in either), and they move in the same direction. Return the list in order; unequal lengths return None.

Why this case matters

Counterpoint checkers and voice-leading validators flag forbidden parallel perfect intervals.

1 / The failure

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

N = 1
observations = []
def solve(x):
    if not isinstance(x, list) or len(x) != 2:
        return None
    up, lo = x
    if len(up) != len(lo):
        return None
    out = []
    for i in range(len(up) - 1):
        a = (up[i] - lo[i]) % 12
        b = (up[i + 1] - lo[i + 1]) % 12
        mu = up[i + 1] - up[i]
        ml = lo[i + 1] - lo[i]
        if a not in (0, 7) or b not in (0, 7):
            continue
        if mu == 0 or ml == 0:
            continue
        if (mu > 0) != (ml > 0):
            continue
        out.append([i, 'P8' if a == 0 else 'P5'])
    return out
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[([[67, 69], [60, 62]], [[0, 'P5']]), ([[72, 74], [60, 62]], [[0, 'P8']]), ([[79, 81], [60, 62]], [[0, 'P5']]), ([[67, 67], [60, 60]], []), ([[67, 69], [60, 60]], []), ([[72, 67], [60, 72]], []), ([[72, 79], [60, 55]], []), ([[67, 74], [60, 62]], [])], [([[67, 67], [60, 60]], []), ([[67, 69], [60, 60]], []), ([[72, 67], [60, 72]], []), ([[72, 79], [60, 55]], []), ([[67, 72], [60, 65]], [[0, 'P5']]), ([[72, 79], [60, 60]], []), ([[60, 62], [67, 69]], []), ([[67, 74], [60, 62]], [])], [([[72, 79], [60, 55]], []), ([[67, 72], [60, 65]], [[0, 'P5']]), ([[72, 79], [60, 60]], []), ([[60, 62], [67, 69]], []), ([[55, 57], [60, 62]], [[0, 'P5']]), ([[67, 69, 71], [60, 62, 64]], [[0, 'P5'], [1, 'P5']]), ([[67, 65], [60, 58]], [[0, 'P5']]), ([[67, 74], [60, 62]], [])], [([[60, 62], [67, 69]], []), ([[55, 57], [60, 62]], [[0, 'P5']]), ([[67, 69, 71], [60, 62, 64]], [[0, 'P5'], [1, 'P5']]), ([[67, 65], [60, 58]], [[0, 'P5']]), ([[72, 72, 74], [60, 60, 62]], [[1, 'P8']]), ([[67, 72], [60, 60]], []), ([[64, 65], [60, 62]], []), ([[67, 74], [60, 62]], [])], [([[67, 65], [60, 58]], [[0, 'P5']]), ([[72, 72, 74], [60, 60, 62]], [[1, 'P8']]), ([[67, 72], [60, 60]], []), ([[64, 65], [60, 62]], []), ([[67], [60]], []), ([[], []], []), ([[67, 69], [60]], None), ([[67, 74], [60, 62]], [])]]
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 0[[0, 'P5']][[0, 'P5']]Passed
oracle 1[[0, 'P8']][[0, 'P8']]Passed
oracle 2[[0, 'P5']][[0, 'P5']]Passed
oracle 3[][]Passed
oracle 4[][]Passed
oracle 5[][]Passed
oracle 6[][]Passed
oracle 7[[0, 'P5']][]Failed

SHA-256 / eca4f53b168f6c9e0c9b1c7f1ff9bc7ecae1453ed2cf4911f2c1f6d21ab997b1

2 / The unsuccessful fix

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

N = 1
observations = []
def solve(x):
    if not isinstance(x, list) or len(x) != 2:
        return None
    up, lo = x
    if len(up) != len(lo):
        return None
    out = []
    for i in range(len(up) - 1):
        a = (up[i] - lo[i]) % 12
        b = (up[i + 1] - lo[i + 1]) % 12
        mu = up[i + 1] - up[i]
        ml = lo[i + 1] - lo[i]
        if a != b and b != 0 or a not in (0, 7):
            continue
        if mu == 0 or ml == 0:
            continue
        if (mu > 0) != (ml > 0):
            continue
        out.append([i, 'P8' if a == 0 else 'P5'])
    return out
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[([[67, 69], [60, 62]], [[0, 'P5']]), ([[72, 74], [60, 62]], [[0, 'P8']]), ([[79, 81], [60, 62]], [[0, 'P5']]), ([[67, 67], [60, 60]], []), ([[67, 69], [60, 60]], []), ([[72, 67], [60, 72]], []), ([[72, 79], [60, 55]], []), ([[67, 74], [60, 62]], [])], [([[67, 67], [60, 60]], []), ([[67, 69], [60, 60]], []), ([[72, 67], [60, 72]], []), ([[72, 79], [60, 55]], []), ([[67, 72], [60, 65]], [[0, 'P5']]), ([[72, 79], [60, 60]], []), ([[60, 62], [67, 69]], []), ([[67, 74], [60, 62]], [])], [([[72, 79], [60, 55]], []), ([[67, 72], [60, 65]], [[0, 'P5']]), ([[72, 79], [60, 60]], []), ([[60, 62], [67, 69]], []), ([[55, 57], [60, 62]], [[0, 'P5']]), ([[67, 69, 71], [60, 62, 64]], [[0, 'P5'], [1, 'P5']]), ([[67, 65], [60, 58]], [[0, 'P5']]), ([[67, 74], [60, 62]], [])], [([[60, 62], [67, 69]], []), ([[55, 57], [60, 62]], [[0, 'P5']]), ([[67, 69, 71], [60, 62, 64]], [[0, 'P5'], [1, 'P5']]), ([[67, 65], [60, 58]], [[0, 'P5']]), ([[72, 72, 74], [60, 60, 62]], [[1, 'P8']]), ([[67, 72], [60, 60]], []), ([[64, 65], [60, 62]], []), ([[67, 74], [60, 62]], [])], [([[67, 65], [60, 58]], [[0, 'P5']]), ([[72, 72, 74], [60, 60, 62]], [[1, 'P8']]), ([[67, 72], [60, 60]], []), ([[64, 65], [60, 62]], []), ([[67], [60]], []), ([[], []], []), ([[67, 69], [60]], None), ([[67, 74], [60, 62]], [])]]
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 0[[0, 'P5']][[0, 'P5']]Passed
oracle 1[[0, 'P8']][[0, 'P8']]Passed
oracle 2[[0, 'P5']][[0, 'P5']]Passed
oracle 3[][]Passed
oracle 4[][]Passed
oracle 5[][]Passed
oracle 6[][]Passed
oracle 7[[0, 'P5']][]Failed

SHA-256 / 2b07784edf3587b16b9479ac79e7e19d88c394a7888a0e0cec8092e405895f6a

3 / The verified repair

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

N = 1
observations = []
def solve(x):
    if not isinstance(x, list) or len(x) != 2:
        return None
    up, lo = x
    if len(up) != len(lo):
        return None
    out = []
    for i in range(len(up) - 1):
        a = (up[i] - lo[i]) % 12
        b = (up[i + 1] - lo[i + 1]) % 12
        mu = up[i + 1] - up[i]
        ml = lo[i + 1] - lo[i]
        if a != b or a not in (0, 7):
            continue
        if mu == 0 or ml == 0:
            continue
        if (mu > 0) != (ml > 0):
            continue
        out.append([i, 'P8' if a == 0 else 'P5'])
    return out
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[([[67, 69], [60, 62]], [[0, 'P5']]), ([[72, 74], [60, 62]], [[0, 'P8']]), ([[79, 81], [60, 62]], [[0, 'P5']]), ([[67, 67], [60, 60]], []), ([[67, 69], [60, 60]], []), ([[72, 67], [60, 72]], []), ([[72, 79], [60, 55]], []), ([[67, 74], [60, 62]], [])], [([[67, 67], [60, 60]], []), ([[67, 69], [60, 60]], []), ([[72, 67], [60, 72]], []), ([[72, 79], [60, 55]], []), ([[67, 72], [60, 65]], [[0, 'P5']]), ([[72, 79], [60, 60]], []), ([[60, 62], [67, 69]], []), ([[67, 74], [60, 62]], [])], [([[72, 79], [60, 55]], []), ([[67, 72], [60, 65]], [[0, 'P5']]), ([[72, 79], [60, 60]], []), ([[60, 62], [67, 69]], []), ([[55, 57], [60, 62]], [[0, 'P5']]), ([[67, 69, 71], [60, 62, 64]], [[0, 'P5'], [1, 'P5']]), ([[67, 65], [60, 58]], [[0, 'P5']]), ([[67, 74], [60, 62]], [])], [([[60, 62], [67, 69]], []), ([[55, 57], [60, 62]], [[0, 'P5']]), ([[67, 69, 71], [60, 62, 64]], [[0, 'P5'], [1, 'P5']]), ([[67, 65], [60, 58]], [[0, 'P5']]), ([[72, 72, 74], [60, 60, 62]], [[1, 'P8']]), ([[67, 72], [60, 60]], []), ([[64, 65], [60, 62]], []), ([[67, 74], [60, 62]], [])], [([[67, 65], [60, 58]], [[0, 'P5']]), ([[72, 72, 74], [60, 60, 62]], [[1, 'P8']]), ([[67, 72], [60, 60]], []), ([[64, 65], [60, 62]], []), ([[67], [60]], []), ([[], []], []), ([[67, 69], [60]], None), ([[67, 74], [60, 62]], [])]]
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 0[[0, 'P5']][[0, 'P5']]Passed
oracle 1[[0, 'P8']][[0, 'P8']]Passed
oracle 2[[0, 'P5']][[0, 'P5']]Passed
oracle 3[][]Passed
oracle 4[][]Passed
oracle 5[][]Passed
oracle 6[][]Passed
oracle 7[][]Passed

SHA-256 / c18867a458ae21b8700d9048176da51f95f4717451389783ec2c5249ff691255

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:50:01.100682+00:00.

Case digest / 5dfd6dc72cc7f3bbba986eef60d45e419d15e4c613502c90ac74c7d6d8d87368