From d8f08ef50b3fef9d65becfc3e0d4525c2efe674f Mon Sep 17 00:00:00 2001 From: Luis Raimundo Date: Sun, 7 Jun 2026 22:42:18 +0100 Subject: [PATCH] Add interval homogeneity regression fixture suite --- .../EDO_24_microtonal_regular.json | 45 + .../EDO_bin_change_sensitivity.json | 48 + .../augmented_triad_symmetric.json | 39 + .../chromatic_cluster_four.json | 44 + .../diminished_seventh_symmetric.json | 44 + .../dominant_seventh_irregular.json | 44 + .../inversion_same_interval_multiset.json | 40 + .../major_triad_close.json | 39 + .../manifest.json | 39 + .../minor_second_dyad.json | 36 + .../octave_duplication_case.json | 39 + .../passage_changing_interval_field.json | 102 ++ ...epeated_pitch_density_not_homogeneity.json | 45 + ..._cardinality_different_distribution_A.json | 44 + ..._cardinality_different_distribution_B.json | 45 + .../single_interval_dyad_perfect_fifth.json | 35 + .../stacked_fifths.json | 44 + .../stacked_fourths.json | 44 + .../transposed_same_structure.json | 45 + .../unison_dyad.json | 38 + .../whole_tone_segment_four.json | 44 + ...val_homogeneity_regression_inspection.json | 1442 +++++++++++++++++ ...erval_homogeneity_regression_inspection.md | 294 ++++ ...nterval_homogeneity_regression_fixtures.py | 240 +++ ...inspect_interval_homogeneity_regression.py | 252 +++ ...NTERVAL_HOMOGENEITY_REGRESSION_FIXTURES.md | 230 +++ ...nterval_homogeneity_regression_fixtures.py | 308 ++++ 27 files changed, 3709 insertions(+) create mode 100644 corpus/fixtures/interval_homogeneity_regression/EDO_24_microtonal_regular.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/EDO_bin_change_sensitivity.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/augmented_triad_symmetric.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/chromatic_cluster_four.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/diminished_seventh_symmetric.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/dominant_seventh_irregular.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/inversion_same_interval_multiset.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/major_triad_close.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/manifest.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/minor_second_dyad.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/octave_duplication_case.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/passage_changing_interval_field.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/repeated_pitch_density_not_homogeneity.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/same_cardinality_different_distribution_A.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/same_cardinality_different_distribution_B.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/single_interval_dyad_perfect_fifth.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/stacked_fifths.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/stacked_fourths.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/transposed_same_structure.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/unison_dyad.json create mode 100644 corpus/fixtures/interval_homogeneity_regression/whole_tone_segment_four.json create mode 100644 corpus/reports/interval_homogeneity_regression_inspection.json create mode 100644 corpus/reports/interval_homogeneity_regression_inspection.md create mode 100644 corpus/scripts/create_interval_homogeneity_regression_fixtures.py create mode 100644 corpus/scripts/inspect_interval_homogeneity_regression.py create mode 100644 docs/INTERVAL_HOMOGENEITY_REGRESSION_FIXTURES.md create mode 100644 tests/test_interval_homogeneity_regression_fixtures.py diff --git a/corpus/fixtures/interval_homogeneity_regression/EDO_24_microtonal_regular.json b/corpus/fixtures/interval_homogeneity_regression/EDO_24_microtonal_regular.json new file mode 100644 index 0000000..c95b476 --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/EDO_24_microtonal_regular.json @@ -0,0 +1,45 @@ +{ + "schema_version": 1, + "fixture_id": "EDO_24_microtonal_regular", + "description": "Quarter-tone chromatic segment on 24-EDO grid (bin_cents=50).", + "tags": [ + "microtonal", + "edo24" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 50, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration", + "edo": 24 + }, + "qualitative": { + "regular_adjacent_spacing": true + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "C", + 0.5, + 4 + ], + [ + "D", + 0.0, + 4 + ], + [ + "D", + 0.5, + 4 + ] + ] +} diff --git a/corpus/fixtures/interval_homogeneity_regression/EDO_bin_change_sensitivity.json b/corpus/fixtures/interval_homogeneity_regression/EDO_bin_change_sensitivity.json new file mode 100644 index 0000000..962290d --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/EDO_bin_change_sensitivity.json @@ -0,0 +1,48 @@ +{ + "schema_version": 1, + "fixture_id": "EDO_bin_change_sensitivity", + "description": "Same four semitones analysed at bin_cents=100 vs 50 (modelling choice).", + "tags": [ + "edo", + "binning" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "qualitative": { + "binning_is_modelling_choice": true + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "C", + 1.0, + 4 + ], + [ + "D", + 0.0, + 4 + ], + [ + "D", + 1.0, + 4 + ] + ], + "bin_variants": [ + 100, + 50 + ] +} diff --git a/corpus/fixtures/interval_homogeneity_regression/augmented_triad_symmetric.json b/corpus/fixtures/interval_homogeneity_regression/augmented_triad_symmetric.json new file mode 100644 index 0000000..b07335b --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/augmented_triad_symmetric.json @@ -0,0 +1,39 @@ +{ + "schema_version": 1, + "fixture_id": "augmented_triad_symmetric", + "description": "Augmented triad C4–E4–G#4: equal major-third division in 12-TET.", + "tags": [ + "triad", + "symmetric" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "qualitative": { + "stronger_than": "major_triad_close" + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "E", + 0.0, + 4 + ], + [ + "G", + 1.0, + 4 + ] + ] +} diff --git a/corpus/fixtures/interval_homogeneity_regression/chromatic_cluster_four.json b/corpus/fixtures/interval_homogeneity_regression/chromatic_cluster_four.json new file mode 100644 index 0000000..a7671b0 --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/chromatic_cluster_four.json @@ -0,0 +1,44 @@ +{ + "schema_version": 1, + "fixture_id": "chromatic_cluster_four", + "description": "Four consecutive semitones C4–D#4: regular adjacent spacing, diverse pairwise set.", + "tags": [ + "cluster", + "adjacent_vs_pairwise" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "qualitative": { + "chain_gt_pair": true + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "C", + 1.0, + 4 + ], + [ + "D", + 0.0, + 4 + ], + [ + "D", + 1.0, + 4 + ] + ] +} diff --git a/corpus/fixtures/interval_homogeneity_regression/diminished_seventh_symmetric.json b/corpus/fixtures/interval_homogeneity_regression/diminished_seventh_symmetric.json new file mode 100644 index 0000000..28e41f7 --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/diminished_seventh_symmetric.json @@ -0,0 +1,44 @@ +{ + "schema_version": 1, + "fixture_id": "diminished_seventh_symmetric", + "description": "Diminished seventh C4–A4: symmetric minor-third tiling.", + "tags": [ + "seventh", + "symmetric" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "qualitative": { + "stronger_than": "dominant_seventh_irregular" + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "E", + -1.0, + 4 + ], + [ + "G", + -1.0, + 4 + ], + [ + "A", + 0.0, + 4 + ] + ] +} diff --git a/corpus/fixtures/interval_homogeneity_regression/dominant_seventh_irregular.json b/corpus/fixtures/interval_homogeneity_regression/dominant_seventh_irregular.json new file mode 100644 index 0000000..611bdcc --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/dominant_seventh_irregular.json @@ -0,0 +1,44 @@ +{ + "schema_version": 1, + "fixture_id": "dominant_seventh_irregular", + "description": "Dominant seventh C4–Bb4: same cardinality as dim7, less regular pairwise profile.", + "tags": [ + "seventh", + "irregular" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "qualitative": { + "same_cardinality_as": "diminished_seventh_symmetric" + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "E", + 0.0, + 4 + ], + [ + "G", + 0.0, + 4 + ], + [ + "B", + -1.0, + 4 + ] + ] +} diff --git a/corpus/fixtures/interval_homogeneity_regression/inversion_same_interval_multiset.json b/corpus/fixtures/interval_homogeneity_regression/inversion_same_interval_multiset.json new file mode 100644 index 0000000..08a84f5 --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/inversion_same_interval_multiset.json @@ -0,0 +1,40 @@ +{ + "schema_version": 1, + "fixture_id": "inversion_same_interval_multiset", + "description": "Inversion of major_triad_close around E4: E4 G#4 C#4.", + "tags": [ + "metamorphic", + "inversion" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "qualitative": { + "interval_multiset_preserved": true + }, + "notes": [ + [ + "E", + 0.0, + 4 + ], + [ + "G", + 1.0, + 4 + ], + [ + "C", + 1.0, + 4 + ] + ], + "reference_fixture": "major_triad_close" +} diff --git a/corpus/fixtures/interval_homogeneity_regression/major_triad_close.json b/corpus/fixtures/interval_homogeneity_regression/major_triad_close.json new file mode 100644 index 0000000..e29e98d --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/major_triad_close.json @@ -0,0 +1,39 @@ +{ + "schema_version": 1, + "fixture_id": "major_triad_close", + "description": "Closed major triad C4–E4–G4: three distinct pairwise interval types.", + "tags": [ + "triad", + "irregular_pairwise" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "qualitative": { + "compare_with": "augmented_triad_symmetric" + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "E", + 0.0, + 4 + ], + [ + "G", + 0.0, + 4 + ] + ] +} diff --git a/corpus/fixtures/interval_homogeneity_regression/manifest.json b/corpus/fixtures/interval_homogeneity_regression/manifest.json new file mode 100644 index 0000000..7d9537a --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/manifest.json @@ -0,0 +1,39 @@ +{ + "schema_version": 1, + "suite": "interval_homogeneity_regression", + "phase": 1, + "fixture_count": 20, + "fixtures": [ + "unison_dyad", + "single_interval_dyad_perfect_fifth", + "minor_second_dyad", + "chromatic_cluster_four", + "whole_tone_segment_four", + "stacked_fourths", + "stacked_fifths", + "major_triad_close", + "augmented_triad_symmetric", + "diminished_seventh_symmetric", + "dominant_seventh_irregular", + "same_cardinality_different_distribution_A", + "same_cardinality_different_distribution_B", + "octave_duplication_case", + "transposed_same_structure", + "inversion_same_interval_multiset", + "EDO_24_microtonal_regular", + "EDO_bin_change_sensitivity", + "repeated_pitch_density_not_homogeneity", + "passage_changing_interval_field" + ], + "default_analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "notes": "Qualitative/metamorphic regression only. No locked scalar golden values in phase 1." +} diff --git a/corpus/fixtures/interval_homogeneity_regression/minor_second_dyad.json b/corpus/fixtures/interval_homogeneity_regression/minor_second_dyad.json new file mode 100644 index 0000000..115069c --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/minor_second_dyad.json @@ -0,0 +1,36 @@ +{ + "schema_version": 1, + "fixture_id": "minor_second_dyad", + "description": "Dyad C4–C#4: single m2 interval; high concentration does not imply consonance.", + "tags": [ + "dyad", + "single_interval", + "dissonant_spelling" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "qualitative": { + "single_interval_concentration": true, + "not_perceptual_consonance": true + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "C", + 1.0, + 4 + ] + ] +} diff --git a/corpus/fixtures/interval_homogeneity_regression/octave_duplication_case.json b/corpus/fixtures/interval_homogeneity_regression/octave_duplication_case.json new file mode 100644 index 0000000..910e219 --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/octave_duplication_case.json @@ -0,0 +1,39 @@ +{ + "schema_version": 1, + "fixture_id": "octave_duplication_case", + "description": "C4 C5 G4: octave duplication; model uses distinct pitch heights (no pc collapse).", + "tags": [ + "octave", + "register" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "qualitative": { + "octave_equivalence_applied": false + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "C", + 0.0, + 5 + ], + [ + "G", + 0.0, + 4 + ] + ] +} diff --git a/corpus/fixtures/interval_homogeneity_regression/passage_changing_interval_field.json b/corpus/fixtures/interval_homogeneity_regression/passage_changing_interval_field.json new file mode 100644 index 0000000..d828225 --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/passage_changing_interval_field.json @@ -0,0 +1,102 @@ +{ + "schema_version": 1, + "fixture_id": "passage_changing_interval_field", + "description": "Three verticalities: chromatic cluster, whole-tone segment, diminished seventh.", + "tags": [ + "passage", + "multi_slice" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "qualitative": { + "changing_interval_profile": true + }, + "slices": [ + { + "slice_id": 1, + "label": "chromatic_cluster", + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "C", + 1.0, + 4 + ], + [ + "D", + 0.0, + 4 + ], + [ + "D", + 1.0, + 4 + ] + ] + }, + { + "slice_id": 2, + "label": "whole_tone_segment", + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "D", + 0.0, + 4 + ], + [ + "E", + 0.0, + 4 + ], + [ + "F", + 1.0, + 4 + ] + ] + }, + { + "slice_id": 3, + "label": "diminished_seventh", + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "E", + -1.0, + 4 + ], + [ + "G", + -1.0, + 4 + ], + [ + "A", + 0.0, + 4 + ] + ] + } + ] +} diff --git a/corpus/fixtures/interval_homogeneity_regression/repeated_pitch_density_not_homogeneity.json b/corpus/fixtures/interval_homogeneity_regression/repeated_pitch_density_not_homogeneity.json new file mode 100644 index 0000000..3647134 --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/repeated_pitch_density_not_homogeneity.json @@ -0,0 +1,45 @@ +{ + "schema_version": 1, + "fixture_id": "repeated_pitch_density_not_homogeneity", + "description": "C4 C4 C4 G4: duplicate pitches preserved in manual aggregate path.", + "tags": [ + "duplicate", + "density" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "qualitative": { + "duplicates_preserved_in_raw_metrics": true, + "dedupe_collapses_to_dyad": true + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "C", + 0.0, + 4 + ], + [ + "C", + 0.0, + 4 + ], + [ + "G", + 0.0, + 4 + ] + ] +} diff --git a/corpus/fixtures/interval_homogeneity_regression/same_cardinality_different_distribution_A.json b/corpus/fixtures/interval_homogeneity_regression/same_cardinality_different_distribution_A.json new file mode 100644 index 0000000..a5b8b9f --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/same_cardinality_different_distribution_A.json @@ -0,0 +1,44 @@ +{ + "schema_version": 1, + "fixture_id": "same_cardinality_different_distribution_A", + "description": "Four-note diatonic cluster C4–F4: compact scalar / adjacent regularity.", + "tags": [ + "cardinality_control", + "compact" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "qualitative": { + "cardinality": 4 + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "D", + 0.0, + 4 + ], + [ + "E", + 0.0, + 4 + ], + [ + "F", + 0.0, + 4 + ] + ] +} diff --git a/corpus/fixtures/interval_homogeneity_regression/same_cardinality_different_distribution_B.json b/corpus/fixtures/interval_homogeneity_regression/same_cardinality_different_distribution_B.json new file mode 100644 index 0000000..424ee78 --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/same_cardinality_different_distribution_B.json @@ -0,0 +1,45 @@ +{ + "schema_version": 1, + "fixture_id": "same_cardinality_different_distribution_B", + "description": "Four-note wide/symmetric C4–C5: same cardinality, different interval distribution.", + "tags": [ + "cardinality_control", + "wide" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "qualitative": { + "cardinality": 4, + "differs_from": "same_cardinality_different_distribution_A" + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "E", + 0.0, + 4 + ], + [ + "G", + 1.0, + 4 + ], + [ + "C", + 0.0, + 5 + ] + ] +} diff --git a/corpus/fixtures/interval_homogeneity_regression/single_interval_dyad_perfect_fifth.json b/corpus/fixtures/interval_homogeneity_regression/single_interval_dyad_perfect_fifth.json new file mode 100644 index 0000000..89d36ca --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/single_interval_dyad_perfect_fifth.json @@ -0,0 +1,35 @@ +{ + "schema_version": 1, + "fixture_id": "single_interval_dyad_perfect_fifth", + "description": "Dyad C4–G4: one pairwise interval type (P5 on 12-TET grid).", + "tags": [ + "dyad", + "single_interval" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "qualitative": { + "single_interval_concentration": true, + "not_perceptual_consonance": true + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "G", + 0.0, + 4 + ] + ] +} diff --git a/corpus/fixtures/interval_homogeneity_regression/stacked_fifths.json b/corpus/fixtures/interval_homogeneity_regression/stacked_fifths.json new file mode 100644 index 0000000..59f4108 --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/stacked_fifths.json @@ -0,0 +1,44 @@ +{ + "schema_version": 1, + "fixture_id": "stacked_fifths", + "description": "Fifths stack C3–A4: repeated P5 adjacent intervals.", + "tags": [ + "quintal", + "regular_adjacent" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "qualitative": { + "chain_gt_pair": true + }, + "notes": [ + [ + "C", + 0.0, + 3 + ], + [ + "G", + 0.0, + 3 + ], + [ + "D", + 0.0, + 4 + ], + [ + "A", + 0.0, + 4 + ] + ] +} diff --git a/corpus/fixtures/interval_homogeneity_regression/stacked_fourths.json b/corpus/fixtures/interval_homogeneity_regression/stacked_fourths.json new file mode 100644 index 0000000..e9eef40 --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/stacked_fourths.json @@ -0,0 +1,44 @@ +{ + "schema_version": 1, + "fixture_id": "stacked_fourths", + "description": "Quartal stack C4–Eb5: repeated P4 adjacent intervals.", + "tags": [ + "quartal", + "regular_adjacent" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "qualitative": { + "chain_gt_pair": true + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "F", + 0.0, + 4 + ], + [ + "B", + -1.0, + 4 + ], + [ + "E", + -1.0, + 5 + ] + ] +} diff --git a/corpus/fixtures/interval_homogeneity_regression/transposed_same_structure.json b/corpus/fixtures/interval_homogeneity_regression/transposed_same_structure.json new file mode 100644 index 0000000..1a27fe7 --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/transposed_same_structure.json @@ -0,0 +1,45 @@ +{ + "schema_version": 1, + "fixture_id": "transposed_same_structure", + "description": "chromatic_cluster_four transposed up major second (D4–F#4).", + "tags": [ + "metamorphic", + "transposition" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "qualitative": { + "transposition_invariant": true + }, + "notes": [ + [ + "D", + 0.0, + 4 + ], + [ + "D", + 1.0, + 4 + ], + [ + "E", + 0.0, + 4 + ], + [ + "E", + 1.0, + 4 + ] + ], + "reference_fixture": "chromatic_cluster_four" +} diff --git a/corpus/fixtures/interval_homogeneity_regression/unison_dyad.json b/corpus/fixtures/interval_homogeneity_regression/unison_dyad.json new file mode 100644 index 0000000..a7feadd --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/unison_dyad.json @@ -0,0 +1,38 @@ +{ + "schema_version": 1, + "fixture_id": "unison_dyad", + "description": "Degenerate dyad: duplicate C4. Two vertical events, one unique pitch height.", + "tags": [ + "dyad", + "degenerate", + "duplicate" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "qualitative": { + "vertical_note_count": 2, + "unique_pitch_heights": 1, + "single_interval_concentration": true, + "not_perceptual_consonance": true + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "C", + 0.0, + 4 + ] + ] +} diff --git a/corpus/fixtures/interval_homogeneity_regression/whole_tone_segment_four.json b/corpus/fixtures/interval_homogeneity_regression/whole_tone_segment_four.json new file mode 100644 index 0000000..e92b3c8 --- /dev/null +++ b/corpus/fixtures/interval_homogeneity_regression/whole_tone_segment_four.json @@ -0,0 +1,44 @@ +{ + "schema_version": 1, + "fixture_id": "whole_tone_segment_four", + "description": "Whole-tone segment C4–F#4: uniform adjacent M2, broader pairwise distances.", + "tags": [ + "whole_tone", + "adjacent_vs_pairwise" + ], + "analysis": { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.6, + "intervallic_headline_mode": "pairwise_intervallic_concentration" + }, + "qualitative": { + "chain_gt_pair": true + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "D", + 0.0, + 4 + ], + [ + "E", + 0.0, + 4 + ], + [ + "F", + 1.0, + 4 + ] + ] +} diff --git a/corpus/reports/interval_homogeneity_regression_inspection.json b/corpus/reports/interval_homogeneity_regression_inspection.json new file mode 100644 index 0000000..aa8657a --- /dev/null +++ b/corpus/reports/interval_homogeneity_regression_inspection.json @@ -0,0 +1,1442 @@ +{ + "generated_at": "2026-06-07T21:12:34Z", + "suite": "interval_homogeneity_regression", + "phase": 1, + "fixture_count": 20, + "fixtures": [ + { + "fixture": "unison_dyad", + "description": "Degenerate dyad: duplicate C4. Two vertical events, one unique pitch height.", + "tags": [ + "dyad", + "degenerate", + "duplicate" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 100, + "chain_threshold": 0.6 + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "C", + 0.0, + 4 + ] + ], + "cardinality": 2, + "vertical_note_count": 2, + "vertical_unique_pitch_count": 1, + "pair_score": 1.0, + "chain_score": 1.0, + "weighted_linear_score": 1.0, + "weighted_quadratic_score": 1.0, + "hybrid_score": 1.0, + "H": 1.0, + "H_label": "homogeneous", + "evenness_score": 0.0, + "type_score": 1.0, + "classification": "P1-dominant; low evenness; uniform stacking", + "distance_counts": { + "0": 1 + }, + "adj_counts": { + "0": 1 + }, + "dominant_pairwise_interval": "P1", + "dominant_adjacent_interval": "P1", + "interpretation_note": "Single-interval or maximally concentrated dyad/degenerate case (not consonance validation)." + }, + { + "fixture": "single_interval_dyad_perfect_fifth", + "description": "Dyad C4–G4: one pairwise interval type (P5 on 12-TET grid).", + "tags": [ + "dyad", + "single_interval" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 100, + "chain_threshold": 0.6 + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "G", + 0.0, + 4 + ] + ], + "cardinality": 2, + "vertical_note_count": 2, + "vertical_unique_pitch_count": 2, + "pair_score": 1.0, + "chain_score": 1.0, + "weighted_linear_score": 1.0, + "weighted_quadratic_score": 1.0, + "hybrid_score": 1.0, + "H": 1.0, + "H_label": "homogeneous", + "evenness_score": 0.0, + "type_score": 1.0, + "classification": "P5-dominant; low evenness; uniform stacking", + "distance_counts": { + "7": 1 + }, + "adj_counts": { + "7": 1 + }, + "dominant_pairwise_interval": "P5", + "dominant_adjacent_interval": "P5", + "interpretation_note": "Single-interval or maximally concentrated dyad/degenerate case (not consonance validation)." + }, + { + "fixture": "minor_second_dyad", + "description": "Dyad C4–C#4: single m2 interval; high concentration does not imply consonance.", + "tags": [ + "dyad", + "single_interval", + "dissonant_spelling" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 100, + "chain_threshold": 0.6 + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "C", + 1.0, + 4 + ] + ], + "cardinality": 2, + "vertical_note_count": 2, + "vertical_unique_pitch_count": 2, + "pair_score": 1.0, + "chain_score": 1.0, + "weighted_linear_score": 1.0, + "weighted_quadratic_score": 1.0, + "hybrid_score": 1.0, + "H": 1.0, + "H_label": "homogeneous", + "evenness_score": 0.0, + "type_score": 1.0, + "classification": "m2-dominant; low evenness; uniform stacking", + "distance_counts": { + "1": 1 + }, + "adj_counts": { + "1": 1 + }, + "dominant_pairwise_interval": "m2", + "dominant_adjacent_interval": "m2", + "interpretation_note": "Single-interval or maximally concentrated dyad/degenerate case (not consonance validation)." + }, + { + "fixture": "chromatic_cluster_four", + "description": "Four consecutive semitones C4–D#4: regular adjacent spacing, diverse pairwise set.", + "tags": [ + "cluster", + "adjacent_vs_pairwise" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 100, + "chain_threshold": 0.6 + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "C", + 1.0, + 4 + ], + [ + "D", + 0.0, + 4 + ], + [ + "D", + 1.0, + 4 + ] + ], + "cardinality": 4, + "vertical_note_count": 4, + "vertical_unique_pitch_count": 4, + "pair_score": 0.5, + "chain_score": 1.0, + "weighted_linear_score": 0.692308, + "weighted_quadratic_score": 0.830769, + "hybrid_score": 0.8, + "H": 0.5, + "H_label": "moderately homogeneous", + "evenness_score": 0.729574, + "type_score": 0.6, + "classification": "m2-dominant; mid evenness; uniform stacking", + "distance_counts": { + "1": 3, + "2": 2, + "3": 1 + }, + "adj_counts": { + "1": 3 + }, + "dominant_pairwise_interval": "m2", + "dominant_adjacent_interval": "m2", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics." + }, + { + "fixture": "whole_tone_segment_four", + "description": "Whole-tone segment C4–F#4: uniform adjacent M2, broader pairwise distances.", + "tags": [ + "whole_tone", + "adjacent_vs_pairwise" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 100, + "chain_threshold": 0.6 + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "D", + 0.0, + 4 + ], + [ + "E", + 0.0, + 4 + ], + [ + "F", + 1.0, + 4 + ] + ], + "cardinality": 4, + "vertical_note_count": 4, + "vertical_unique_pitch_count": 4, + "pair_score": 0.5, + "chain_score": 1.0, + "weighted_linear_score": 0.692308, + "weighted_quadratic_score": 0.830769, + "hybrid_score": 0.8, + "H": 0.5, + "H_label": "moderately homogeneous", + "evenness_score": 0.519759, + "type_score": 0.6, + "classification": "M2-dominant; mid evenness; uniform stacking", + "distance_counts": { + "2": 3, + "4": 2, + "6": 1 + }, + "adj_counts": { + "2": 3 + }, + "dominant_pairwise_interval": "M2", + "dominant_adjacent_interval": "M2", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics." + }, + { + "fixture": "stacked_fourths", + "description": "Quartal stack C4–Eb5: repeated P4 adjacent intervals.", + "tags": [ + "quartal", + "regular_adjacent" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 100, + "chain_threshold": 0.6 + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "F", + 0.0, + 4 + ], + [ + "B", + -1.0, + 4 + ], + [ + "E", + -1.0, + 5 + ] + ], + "cardinality": 4, + "vertical_note_count": 4, + "vertical_unique_pitch_count": 4, + "pair_score": 0.5, + "chain_score": 1.0, + "weighted_linear_score": 0.692308, + "weighted_quadratic_score": 0.830769, + "hybrid_score": 0.8, + "H": 0.5, + "H_label": "moderately homogeneous", + "evenness_score": 0.364787, + "type_score": 0.6, + "classification": "P4-dominant; mid evenness; uniform stacking", + "distance_counts": { + "5": 3, + "10": 2, + "15": 1 + }, + "adj_counts": { + "5": 3 + }, + "dominant_pairwise_interval": "P4", + "dominant_adjacent_interval": "P4", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics." + }, + { + "fixture": "stacked_fifths", + "description": "Fifths stack C3–A4: repeated P5 adjacent intervals.", + "tags": [ + "quintal", + "regular_adjacent" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 100, + "chain_threshold": 0.6 + }, + "notes": [ + [ + "C", + 0.0, + 3 + ], + [ + "G", + 0.0, + 3 + ], + [ + "D", + 0.0, + 4 + ], + [ + "A", + 0.0, + 4 + ] + ], + "cardinality": 4, + "vertical_note_count": 4, + "vertical_unique_pitch_count": 4, + "pair_score": 0.5, + "chain_score": 1.0, + "weighted_linear_score": 0.692308, + "weighted_quadratic_score": 0.830769, + "hybrid_score": 0.8, + "H": 0.5, + "H_label": "moderately homogeneous", + "evenness_score": 0.327205, + "type_score": 0.6, + "classification": "P5-dominant; mid evenness; uniform stacking", + "distance_counts": { + "7": 3, + "14": 2, + "21": 1 + }, + "adj_counts": { + "7": 3 + }, + "dominant_pairwise_interval": "P5", + "dominant_adjacent_interval": "P5", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics." + }, + { + "fixture": "major_triad_close", + "description": "Closed major triad C4–E4–G4: three distinct pairwise interval types.", + "tags": [ + "triad", + "irregular_pairwise" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 100, + "chain_threshold": 0.6 + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "E", + 0.0, + 4 + ], + [ + "G", + 0.0, + 4 + ] + ], + "cardinality": 3, + "vertical_note_count": 3, + "vertical_unique_pitch_count": 3, + "pair_score": 0.333333, + "chain_score": 0.5, + "weighted_linear_score": 0.4, + "weighted_quadratic_score": 0.444444, + "hybrid_score": 0.433333, + "H": 0.333333, + "H_label": "moderately heterogeneous", + "evenness_score": 0.528321, + "type_score": 0.0, + "classification": "no dominant interval; mid evenness; irregular stacking", + "distance_counts": { + "4": 1, + "7": 1, + "3": 1 + }, + "adj_counts": { + "4": 1, + "3": 1 + }, + "dominant_pairwise_interval": "M3", + "dominant_adjacent_interval": "M3", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics." + }, + { + "fixture": "augmented_triad_symmetric", + "description": "Augmented triad C4–E4–G#4: equal major-third division in 12-TET.", + "tags": [ + "triad", + "symmetric" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 100, + "chain_threshold": 0.6 + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "E", + 0.0, + 4 + ], + [ + "G", + 1.0, + 4 + ] + ], + "cardinality": 3, + "vertical_note_count": 3, + "vertical_unique_pitch_count": 3, + "pair_score": 0.666667, + "chain_score": 1.0, + "weighted_linear_score": 0.8, + "weighted_quadratic_score": 0.888889, + "hybrid_score": 0.866667, + "H": 0.666667, + "H_label": "moderately homogeneous", + "evenness_score": 0.28969, + "type_score": 0.5, + "classification": "M3-dominant; low evenness; uniform stacking", + "distance_counts": { + "4": 2, + "8": 1 + }, + "adj_counts": { + "4": 2 + }, + "dominant_pairwise_interval": "M3", + "dominant_adjacent_interval": "M3", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics." + }, + { + "fixture": "diminished_seventh_symmetric", + "description": "Diminished seventh C4–A4: symmetric minor-third tiling.", + "tags": [ + "seventh", + "symmetric" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 100, + "chain_threshold": 0.6 + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "E", + -1.0, + 4 + ], + [ + "G", + -1.0, + 4 + ], + [ + "A", + 0.0, + 4 + ] + ], + "cardinality": 4, + "vertical_note_count": 4, + "vertical_unique_pitch_count": 4, + "pair_score": 0.5, + "chain_score": 1.0, + "weighted_linear_score": 0.692308, + "weighted_quadratic_score": 0.830769, + "hybrid_score": 0.8, + "H": 0.5, + "H_label": "moderately homogeneous", + "evenness_score": 0.439247, + "type_score": 0.6, + "classification": "m3-dominant; mid evenness; uniform stacking", + "distance_counts": { + "3": 3, + "6": 2, + "9": 1 + }, + "adj_counts": { + "3": 3 + }, + "dominant_pairwise_interval": "m3", + "dominant_adjacent_interval": "m3", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics." + }, + { + "fixture": "dominant_seventh_irregular", + "description": "Dominant seventh C4–Bb4: same cardinality as dim7, less regular pairwise profile.", + "tags": [ + "seventh", + "irregular" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 100, + "chain_threshold": 0.6 + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "E", + 0.0, + 4 + ], + [ + "G", + 0.0, + 4 + ], + [ + "B", + -1.0, + 4 + ] + ], + "cardinality": 4, + "vertical_note_count": 4, + "vertical_unique_pitch_count": 4, + "pair_score": 0.333333, + "chain_score": 0.666667, + "weighted_linear_score": 0.461538, + "weighted_quadratic_score": 0.553846, + "hybrid_score": 0.533333, + "H": 0.333333, + "H_label": "moderately heterogeneous", + "evenness_score": 0.650867, + "type_score": 0.2, + "classification": "no dominant interval; mid evenness; predominantly regular stacking", + "distance_counts": { + "4": 1, + "7": 1, + "10": 1, + "3": 2, + "6": 1 + }, + "adj_counts": { + "4": 1, + "3": 2 + }, + "dominant_pairwise_interval": "m3", + "dominant_adjacent_interval": "m3", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics." + }, + { + "fixture": "same_cardinality_different_distribution_A", + "description": "Four-note diatonic cluster C4–F4: compact scalar / adjacent regularity.", + "tags": [ + "cardinality_control", + "compact" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 100, + "chain_threshold": 0.6 + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "D", + 0.0, + 4 + ], + [ + "E", + 0.0, + 4 + ], + [ + "F", + 0.0, + 4 + ] + ], + "cardinality": 4, + "vertical_note_count": 4, + "vertical_unique_pitch_count": 4, + "pair_score": 0.333333, + "chain_score": 0.666667, + "weighted_linear_score": 0.461538, + "weighted_quadratic_score": 0.553846, + "hybrid_score": 0.533333, + "H": 0.333333, + "H_label": "moderately heterogeneous", + "evenness_score": 0.871049, + "type_score": 0.2, + "classification": "no dominant interval; high evenness; predominantly regular stacking", + "distance_counts": { + "2": 2, + "4": 1, + "5": 1, + "3": 1, + "1": 1 + }, + "adj_counts": { + "2": 2, + "1": 1 + }, + "dominant_pairwise_interval": "M2", + "dominant_adjacent_interval": "M2", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics." + }, + { + "fixture": "same_cardinality_different_distribution_B", + "description": "Four-note wide/symmetric C4–C5: same cardinality, different interval distribution.", + "tags": [ + "cardinality_control", + "wide" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 100, + "chain_threshold": 0.6 + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "E", + 0.0, + 4 + ], + [ + "G", + 1.0, + 4 + ], + [ + "C", + 0.0, + 5 + ] + ], + "cardinality": 4, + "vertical_note_count": 4, + "vertical_unique_pitch_count": 4, + "pair_score": 0.5, + "chain_score": 1.0, + "weighted_linear_score": 0.692308, + "weighted_quadratic_score": 0.830769, + "hybrid_score": 0.8, + "H": 0.5, + "H_label": "moderately homogeneous", + "evenness_score": 0.394317, + "type_score": 0.6, + "classification": "M3-dominant; mid evenness; uniform stacking", + "distance_counts": { + "4": 3, + "8": 2, + "12": 1 + }, + "adj_counts": { + "4": 3 + }, + "dominant_pairwise_interval": "M3", + "dominant_adjacent_interval": "M3", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics." + }, + { + "fixture": "octave_duplication_case", + "description": "C4 C5 G4: octave duplication; model uses distinct pitch heights (no pc collapse).", + "tags": [ + "octave", + "register" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 100, + "chain_threshold": 0.6 + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "C", + 0.0, + 5 + ], + [ + "G", + 0.0, + 4 + ] + ], + "cardinality": 3, + "vertical_note_count": 3, + "vertical_unique_pitch_count": 3, + "pair_score": 0.333333, + "chain_score": 0.5, + "weighted_linear_score": 0.4, + "weighted_quadratic_score": 0.444444, + "hybrid_score": 0.433333, + "H": 0.333333, + "H_label": "moderately heterogeneous", + "evenness_score": 0.428317, + "type_score": 0.0, + "classification": "no dominant interval; mid evenness; irregular stacking", + "distance_counts": { + "12": 1, + "7": 1, + "5": 1 + }, + "adj_counts": { + "7": 1, + "5": 1 + }, + "dominant_pairwise_interval": "P8", + "dominant_adjacent_interval": "P5", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics." + }, + { + "fixture": "transposed_same_structure", + "description": "chromatic_cluster_four transposed up major second (D4–F#4).", + "tags": [ + "metamorphic", + "transposition" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 100, + "chain_threshold": 0.6 + }, + "notes": [ + [ + "D", + 0.0, + 4 + ], + [ + "D", + 1.0, + 4 + ], + [ + "E", + 0.0, + 4 + ], + [ + "E", + 1.0, + 4 + ] + ], + "cardinality": 4, + "vertical_note_count": 4, + "vertical_unique_pitch_count": 4, + "pair_score": 0.5, + "chain_score": 1.0, + "weighted_linear_score": 0.692308, + "weighted_quadratic_score": 0.830769, + "hybrid_score": 0.8, + "H": 0.5, + "H_label": "moderately homogeneous", + "evenness_score": 0.729574, + "type_score": 0.6, + "classification": "m2-dominant; mid evenness; uniform stacking", + "distance_counts": { + "1": 3, + "2": 2, + "3": 1 + }, + "adj_counts": { + "1": 3 + }, + "dominant_pairwise_interval": "m2", + "dominant_adjacent_interval": "m2", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics.", + "reference_fixture": "chromatic_cluster_four", + "reference_metrics": { + "cardinality": 4, + "vertical_note_count": 4, + "vertical_unique_pitch_count": 4, + "pair_score": 0.5, + "chain_score": 1.0, + "weighted_linear_score": 0.692308, + "weighted_quadratic_score": 0.830769, + "hybrid_score": 0.8, + "H": 0.5, + "H_label": "moderately homogeneous", + "evenness_score": 0.729574, + "type_score": 0.6, + "classification": "m2-dominant; mid evenness; uniform stacking", + "distance_counts": { + "1": 3, + "2": 2, + "3": 1 + }, + "adj_counts": { + "1": 3 + }, + "dominant_pairwise_interval": "m2", + "dominant_adjacent_interval": "m2", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics." + } + }, + { + "fixture": "inversion_same_interval_multiset", + "description": "Inversion of major_triad_close around E4: E4 G#4 C#4.", + "tags": [ + "metamorphic", + "inversion" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 100, + "chain_threshold": 0.6 + }, + "notes": [ + [ + "E", + 0.0, + 4 + ], + [ + "G", + 1.0, + 4 + ], + [ + "C", + 1.0, + 4 + ] + ], + "cardinality": 3, + "vertical_note_count": 3, + "vertical_unique_pitch_count": 3, + "pair_score": 0.333333, + "chain_score": 0.5, + "weighted_linear_score": 0.4, + "weighted_quadratic_score": 0.444444, + "hybrid_score": 0.433333, + "H": 0.333333, + "H_label": "moderately heterogeneous", + "evenness_score": 0.528321, + "type_score": 0.0, + "classification": "no dominant interval; mid evenness; irregular stacking", + "distance_counts": { + "4": 1, + "3": 1, + "7": 1 + }, + "adj_counts": { + "3": 1, + "4": 1 + }, + "dominant_pairwise_interval": "M3", + "dominant_adjacent_interval": "m3", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics.", + "reference_fixture": "major_triad_close", + "reference_metrics": { + "cardinality": 3, + "vertical_note_count": 3, + "vertical_unique_pitch_count": 3, + "pair_score": 0.333333, + "chain_score": 0.5, + "weighted_linear_score": 0.4, + "weighted_quadratic_score": 0.444444, + "hybrid_score": 0.433333, + "H": 0.333333, + "H_label": "moderately heterogeneous", + "evenness_score": 0.528321, + "type_score": 0.0, + "classification": "no dominant interval; mid evenness; irregular stacking", + "distance_counts": { + "4": 1, + "7": 1, + "3": 1 + }, + "adj_counts": { + "4": 1, + "3": 1 + }, + "dominant_pairwise_interval": "M3", + "dominant_adjacent_interval": "M3", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics." + } + }, + { + "fixture": "EDO_24_microtonal_regular", + "description": "Quarter-tone chromatic segment on 24-EDO grid (bin_cents=50).", + "tags": [ + "microtonal", + "edo24" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 50, + "chain_threshold": 0.6 + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "C", + 0.5, + 4 + ], + [ + "D", + 0.0, + 4 + ], + [ + "D", + 0.5, + 4 + ] + ], + "cardinality": 4, + "vertical_note_count": 4, + "vertical_unique_pitch_count": 4, + "pair_score": 0.333333, + "chain_score": 0.666667, + "weighted_linear_score": 0.461538, + "weighted_quadratic_score": 0.553846, + "hybrid_score": 0.533333, + "H": 0.333333, + "H_label": "moderately heterogeneous", + "evenness_score": 0.742098, + "type_score": 0.4, + "classification": "no dominant interval; mid evenness; predominantly regular stacking", + "distance_counts": { + "1": 2, + "4": 2, + "5": 1, + "3": 1 + }, + "adj_counts": { + "1": 2, + "3": 1 + }, + "dominant_pairwise_interval": "50c", + "dominant_adjacent_interval": "50c", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics." + }, + { + "fixture": "EDO_bin_change_sensitivity", + "description": "Same four semitones analysed at bin_cents=100 vs 50 (modelling choice).", + "tags": [ + "edo", + "binning" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 100, + "chain_threshold": 0.6 + }, + "bin_variants": [ + { + "cardinality": 4, + "vertical_note_count": 4, + "vertical_unique_pitch_count": 4, + "pair_score": 0.5, + "chain_score": 1.0, + "weighted_linear_score": 0.692308, + "weighted_quadratic_score": 0.830769, + "hybrid_score": 0.8, + "H": 0.5, + "H_label": "moderately homogeneous", + "evenness_score": 0.729574, + "type_score": 0.6, + "classification": "m2-dominant; mid evenness; uniform stacking", + "distance_counts": { + "1": 3, + "2": 2, + "3": 1 + }, + "adj_counts": { + "1": 3 + }, + "dominant_pairwise_interval": "m2", + "dominant_adjacent_interval": "m2", + "interpretation_note": "Observed at bin_cents=100; binning is a modelling choice, not tuning.", + "bin_cents": 100 + }, + { + "cardinality": 4, + "vertical_note_count": 4, + "vertical_unique_pitch_count": 4, + "pair_score": 0.5, + "chain_score": 1.0, + "weighted_linear_score": 0.692308, + "weighted_quadratic_score": 0.830769, + "hybrid_score": 0.8, + "H": 0.5, + "H_label": "moderately homogeneous", + "evenness_score": 0.519759, + "type_score": 0.6, + "classification": "100c-dominant; mid evenness; uniform stacking", + "distance_counts": { + "2": 3, + "4": 2, + "6": 1 + }, + "adj_counts": { + "2": 3 + }, + "dominant_pairwise_interval": "100c", + "dominant_adjacent_interval": "100c", + "interpretation_note": "Observed at bin_cents=50; binning is a modelling choice, not tuning.", + "bin_cents": 50 + } + ], + "labels_or_metrics_differ": true + }, + { + "fixture": "repeated_pitch_density_not_homogeneity", + "description": "C4 C4 C4 G4: duplicate pitches preserved in manual aggregate path.", + "tags": [ + "duplicate", + "density" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 100, + "chain_threshold": 0.6 + }, + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "C", + 0.0, + 4 + ], + [ + "C", + 0.0, + 4 + ], + [ + "G", + 0.0, + 4 + ] + ], + "cardinality": 4, + "vertical_note_count": 4, + "vertical_unique_pitch_count": 2, + "pair_score": 0.5, + "chain_score": 0.666667, + "weighted_linear_score": 0.576923, + "weighted_quadratic_score": 0.623077, + "hybrid_score": 0.6, + "H": 0.5, + "H_label": "moderately homogeneous", + "evenness_score": 0.333333, + "type_score": 0.8, + "classification": "P1-dominant; mid evenness; predominantly regular stacking", + "distance_counts": { + "0": 3, + "7": 3 + }, + "adj_counts": { + "0": 2, + "7": 1 + }, + "dominant_pairwise_interval": "P1", + "dominant_adjacent_interval": "P1", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics.", + "deduped_note_count": 2, + "deduped_metrics": { + "cardinality": 2, + "vertical_note_count": 2, + "vertical_unique_pitch_count": 2, + "pair_score": 1.0, + "chain_score": 1.0, + "weighted_linear_score": 1.0, + "weighted_quadratic_score": 1.0, + "hybrid_score": 1.0, + "H": 1.0, + "H_label": "homogeneous", + "evenness_score": 0.0, + "type_score": 1.0, + "classification": "P5-dominant; low evenness; uniform stacking", + "distance_counts": { + "7": 1 + }, + "adj_counts": { + "7": 1 + }, + "dominant_pairwise_interval": "P5", + "dominant_adjacent_interval": "P5", + "interpretation_note": "Single-interval or maximally concentrated dyad/degenerate case (not consonance validation)." + } + }, + { + "fixture": "passage_changing_interval_field", + "description": "Three verticalities: chromatic cluster, whole-tone segment, diminished seventh.", + "tags": [ + "passage", + "multi_slice" + ], + "analysis": { + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "hybrid_alpha": 0.6, + "homogeneity_method": "dominance", + "bin_cents": 100, + "chain_threshold": 0.6 + }, + "slices": [ + { + "cardinality": 4, + "vertical_note_count": 4, + "vertical_unique_pitch_count": 4, + "pair_score": 0.5, + "chain_score": 1.0, + "weighted_linear_score": 0.692308, + "weighted_quadratic_score": 0.830769, + "hybrid_score": 0.8, + "H": 0.5, + "H_label": "moderately homogeneous", + "evenness_score": 0.729574, + "type_score": 0.6, + "classification": "m2-dominant; mid evenness; uniform stacking", + "distance_counts": { + "1": 3, + "2": 2, + "3": 1 + }, + "adj_counts": { + "1": 3 + }, + "dominant_pairwise_interval": "m2", + "dominant_adjacent_interval": "m2", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics.", + "slice_id": 1, + "label": "chromatic_cluster", + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "C", + 1.0, + 4 + ], + [ + "D", + 0.0, + 4 + ], + [ + "D", + 1.0, + 4 + ] + ] + }, + { + "cardinality": 4, + "vertical_note_count": 4, + "vertical_unique_pitch_count": 4, + "pair_score": 0.5, + "chain_score": 1.0, + "weighted_linear_score": 0.692308, + "weighted_quadratic_score": 0.830769, + "hybrid_score": 0.8, + "H": 0.5, + "H_label": "moderately homogeneous", + "evenness_score": 0.519759, + "type_score": 0.6, + "classification": "M2-dominant; mid evenness; uniform stacking", + "distance_counts": { + "2": 3, + "4": 2, + "6": 1 + }, + "adj_counts": { + "2": 3 + }, + "dominant_pairwise_interval": "M2", + "dominant_adjacent_interval": "M2", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics.", + "slice_id": 2, + "label": "whole_tone_segment", + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "D", + 0.0, + 4 + ], + [ + "E", + 0.0, + 4 + ], + [ + "F", + 1.0, + 4 + ] + ] + }, + { + "cardinality": 4, + "vertical_note_count": 4, + "vertical_unique_pitch_count": 4, + "pair_score": 0.5, + "chain_score": 1.0, + "weighted_linear_score": 0.692308, + "weighted_quadratic_score": 0.830769, + "hybrid_score": 0.8, + "H": 0.5, + "H_label": "moderately homogeneous", + "evenness_score": 0.439247, + "type_score": 0.6, + "classification": "m3-dominant; mid evenness; uniform stacking", + "distance_counts": { + "3": 3, + "6": 2, + "9": 1 + }, + "adj_counts": { + "3": 3 + }, + "dominant_pairwise_interval": "m3", + "dominant_adjacent_interval": "m3", + "interpretation_note": "Adjacent regularity exceeds pairwise concentration under current semantics.", + "slice_id": 3, + "label": "diminished_seventh", + "notes": [ + [ + "C", + 0.0, + 4 + ], + [ + "E", + -1.0, + 4 + ], + [ + "G", + -1.0, + 4 + ], + [ + "A", + 0.0, + 4 + ] + ] + } + ], + "passage_delta_rows": [ + { + "Slice": 1, + "Label": "chromatic_cluster", + "H (interval dominance)": 0.5, + "ΔH (prev slice)": null + }, + { + "Slice": 2, + "Label": "whole_tone_segment", + "H (interval dominance)": 0.5, + "ΔH (prev slice)": 0.0 + }, + { + "Slice": 3, + "Label": "diminished_seventh", + "H (interval dominance)": 0.5, + "ΔH (prev slice)": 0.0 + } + ], + "interval_profile_changes": true + } + ] +} diff --git a/corpus/reports/interval_homogeneity_regression_inspection.md b/corpus/reports/interval_homogeneity_regression_inspection.md new file mode 100644 index 0000000..1299dd8 --- /dev/null +++ b/corpus/reports/interval_homogeneity_regression_inspection.md @@ -0,0 +1,294 @@ +# Interval homogeneity regression — exploratory inspection (phase 1) + +Generated: 2026-06-07T21:12:34Z + +These values are **exploratory** only. Do not promote them to strict golden references. + +## unison_dyad + +**Description:** Degenerate dyad: duplicate C4. Two vertical events, one unique pitch height. + +- Notes: `[['C', 0.0, 4], ['C', 0.0, 4]]` +- Cardinality: 2; unique pitches: 1 +- pair_score: 1.0; chain_score: 1.0 +- weighted_linear_score: 1.0 +- hybrid_score: 1.0; H: 1.0; H_label: homogeneous +- evenness_score: 0.0; classification: P1-dominant; low evenness; uniform stacking +- distance_counts: {'0': 1} +- adj_counts: {'0': 1} +- Note: Single-interval or maximally concentrated dyad/degenerate case (not consonance validation). + +## single_interval_dyad_perfect_fifth + +**Description:** Dyad C4–G4: one pairwise interval type (P5 on 12-TET grid). + +- Notes: `[['C', 0.0, 4], ['G', 0.0, 4]]` +- Cardinality: 2; unique pitches: 2 +- pair_score: 1.0; chain_score: 1.0 +- weighted_linear_score: 1.0 +- hybrid_score: 1.0; H: 1.0; H_label: homogeneous +- evenness_score: 0.0; classification: P5-dominant; low evenness; uniform stacking +- distance_counts: {'7': 1} +- adj_counts: {'7': 1} +- Note: Single-interval or maximally concentrated dyad/degenerate case (not consonance validation). + +## minor_second_dyad + +**Description:** Dyad C4–C#4: single m2 interval; high concentration does not imply consonance. + +- Notes: `[['C', 0.0, 4], ['C', 1.0, 4]]` +- Cardinality: 2; unique pitches: 2 +- pair_score: 1.0; chain_score: 1.0 +- weighted_linear_score: 1.0 +- hybrid_score: 1.0; H: 1.0; H_label: homogeneous +- evenness_score: 0.0; classification: m2-dominant; low evenness; uniform stacking +- distance_counts: {'1': 1} +- adj_counts: {'1': 1} +- Note: Single-interval or maximally concentrated dyad/degenerate case (not consonance validation). + +## chromatic_cluster_four + +**Description:** Four consecutive semitones C4–D#4: regular adjacent spacing, diverse pairwise set. + +- Notes: `[['C', 0.0, 4], ['C', 1.0, 4], ['D', 0.0, 4], ['D', 1.0, 4]]` +- Cardinality: 4; unique pitches: 4 +- pair_score: 0.5; chain_score: 1.0 +- weighted_linear_score: 0.692308 +- hybrid_score: 0.8; H: 0.5; H_label: moderately homogeneous +- evenness_score: 0.729574; classification: m2-dominant; mid evenness; uniform stacking +- distance_counts: {'1': 3, '2': 2, '3': 1} +- adj_counts: {'1': 3} +- Note: Adjacent regularity exceeds pairwise concentration under current semantics. + +## whole_tone_segment_four + +**Description:** Whole-tone segment C4–F#4: uniform adjacent M2, broader pairwise distances. + +- Notes: `[['C', 0.0, 4], ['D', 0.0, 4], ['E', 0.0, 4], ['F', 1.0, 4]]` +- Cardinality: 4; unique pitches: 4 +- pair_score: 0.5; chain_score: 1.0 +- weighted_linear_score: 0.692308 +- hybrid_score: 0.8; H: 0.5; H_label: moderately homogeneous +- evenness_score: 0.519759; classification: M2-dominant; mid evenness; uniform stacking +- distance_counts: {'2': 3, '4': 2, '6': 1} +- adj_counts: {'2': 3} +- Note: Adjacent regularity exceeds pairwise concentration under current semantics. + +## stacked_fourths + +**Description:** Quartal stack C4–Eb5: repeated P4 adjacent intervals. + +- Notes: `[['C', 0.0, 4], ['F', 0.0, 4], ['B', -1.0, 4], ['E', -1.0, 5]]` +- Cardinality: 4; unique pitches: 4 +- pair_score: 0.5; chain_score: 1.0 +- weighted_linear_score: 0.692308 +- hybrid_score: 0.8; H: 0.5; H_label: moderately homogeneous +- evenness_score: 0.364787; classification: P4-dominant; mid evenness; uniform stacking +- distance_counts: {'5': 3, '10': 2, '15': 1} +- adj_counts: {'5': 3} +- Note: Adjacent regularity exceeds pairwise concentration under current semantics. + +## stacked_fifths + +**Description:** Fifths stack C3–A4: repeated P5 adjacent intervals. + +- Notes: `[['C', 0.0, 3], ['G', 0.0, 3], ['D', 0.0, 4], ['A', 0.0, 4]]` +- Cardinality: 4; unique pitches: 4 +- pair_score: 0.5; chain_score: 1.0 +- weighted_linear_score: 0.692308 +- hybrid_score: 0.8; H: 0.5; H_label: moderately homogeneous +- evenness_score: 0.327205; classification: P5-dominant; mid evenness; uniform stacking +- distance_counts: {'7': 3, '14': 2, '21': 1} +- adj_counts: {'7': 3} +- Note: Adjacent regularity exceeds pairwise concentration under current semantics. + +## major_triad_close + +**Description:** Closed major triad C4–E4–G4: three distinct pairwise interval types. + +- Notes: `[['C', 0.0, 4], ['E', 0.0, 4], ['G', 0.0, 4]]` +- Cardinality: 3; unique pitches: 3 +- pair_score: 0.333333; chain_score: 0.5 +- weighted_linear_score: 0.4 +- hybrid_score: 0.433333; H: 0.333333; H_label: moderately heterogeneous +- evenness_score: 0.528321; classification: no dominant interval; mid evenness; irregular stacking +- distance_counts: {'4': 1, '7': 1, '3': 1} +- adj_counts: {'4': 1, '3': 1} +- Note: Adjacent regularity exceeds pairwise concentration under current semantics. + +## augmented_triad_symmetric + +**Description:** Augmented triad C4–E4–G#4: equal major-third division in 12-TET. + +- Notes: `[['C', 0.0, 4], ['E', 0.0, 4], ['G', 1.0, 4]]` +- Cardinality: 3; unique pitches: 3 +- pair_score: 0.666667; chain_score: 1.0 +- weighted_linear_score: 0.8 +- hybrid_score: 0.866667; H: 0.666667; H_label: moderately homogeneous +- evenness_score: 0.28969; classification: M3-dominant; low evenness; uniform stacking +- distance_counts: {'4': 2, '8': 1} +- adj_counts: {'4': 2} +- Note: Adjacent regularity exceeds pairwise concentration under current semantics. + +## diminished_seventh_symmetric + +**Description:** Diminished seventh C4–A4: symmetric minor-third tiling. + +- Notes: `[['C', 0.0, 4], ['E', -1.0, 4], ['G', -1.0, 4], ['A', 0.0, 4]]` +- Cardinality: 4; unique pitches: 4 +- pair_score: 0.5; chain_score: 1.0 +- weighted_linear_score: 0.692308 +- hybrid_score: 0.8; H: 0.5; H_label: moderately homogeneous +- evenness_score: 0.439247; classification: m3-dominant; mid evenness; uniform stacking +- distance_counts: {'3': 3, '6': 2, '9': 1} +- adj_counts: {'3': 3} +- Note: Adjacent regularity exceeds pairwise concentration under current semantics. + +## dominant_seventh_irregular + +**Description:** Dominant seventh C4–Bb4: same cardinality as dim7, less regular pairwise profile. + +- Notes: `[['C', 0.0, 4], ['E', 0.0, 4], ['G', 0.0, 4], ['B', -1.0, 4]]` +- Cardinality: 4; unique pitches: 4 +- pair_score: 0.333333; chain_score: 0.666667 +- weighted_linear_score: 0.461538 +- hybrid_score: 0.533333; H: 0.333333; H_label: moderately heterogeneous +- evenness_score: 0.650867; classification: no dominant interval; mid evenness; predominantly regular stacking +- distance_counts: {'4': 1, '7': 1, '10': 1, '3': 2, '6': 1} +- adj_counts: {'4': 1, '3': 2} +- Note: Adjacent regularity exceeds pairwise concentration under current semantics. + +## same_cardinality_different_distribution_A + +**Description:** Four-note diatonic cluster C4–F4: compact scalar / adjacent regularity. + +- Notes: `[['C', 0.0, 4], ['D', 0.0, 4], ['E', 0.0, 4], ['F', 0.0, 4]]` +- Cardinality: 4; unique pitches: 4 +- pair_score: 0.333333; chain_score: 0.666667 +- weighted_linear_score: 0.461538 +- hybrid_score: 0.533333; H: 0.333333; H_label: moderately heterogeneous +- evenness_score: 0.871049; classification: no dominant interval; high evenness; predominantly regular stacking +- distance_counts: {'2': 2, '4': 1, '5': 1, '3': 1, '1': 1} +- adj_counts: {'2': 2, '1': 1} +- Note: Adjacent regularity exceeds pairwise concentration under current semantics. + +## same_cardinality_different_distribution_B + +**Description:** Four-note wide/symmetric C4–C5: same cardinality, different interval distribution. + +- Notes: `[['C', 0.0, 4], ['E', 0.0, 4], ['G', 1.0, 4], ['C', 0.0, 5]]` +- Cardinality: 4; unique pitches: 4 +- pair_score: 0.5; chain_score: 1.0 +- weighted_linear_score: 0.692308 +- hybrid_score: 0.8; H: 0.5; H_label: moderately homogeneous +- evenness_score: 0.394317; classification: M3-dominant; mid evenness; uniform stacking +- distance_counts: {'4': 3, '8': 2, '12': 1} +- adj_counts: {'4': 3} +- Note: Adjacent regularity exceeds pairwise concentration under current semantics. + +## octave_duplication_case + +**Description:** C4 C5 G4: octave duplication; model uses distinct pitch heights (no pc collapse). + +- Notes: `[['C', 0.0, 4], ['C', 0.0, 5], ['G', 0.0, 4]]` +- Cardinality: 3; unique pitches: 3 +- pair_score: 0.333333; chain_score: 0.5 +- weighted_linear_score: 0.4 +- hybrid_score: 0.433333; H: 0.333333; H_label: moderately heterogeneous +- evenness_score: 0.428317; classification: no dominant interval; mid evenness; irregular stacking +- distance_counts: {'12': 1, '7': 1, '5': 1} +- adj_counts: {'7': 1, '5': 1} +- Note: Adjacent regularity exceeds pairwise concentration under current semantics. + +## transposed_same_structure + +**Description:** chromatic_cluster_four transposed up major second (D4–F#4). + +- Notes: `[['D', 0.0, 4], ['D', 1.0, 4], ['E', 0.0, 4], ['E', 1.0, 4]]` +- Cardinality: 4; unique pitches: 4 +- pair_score: 0.5; chain_score: 1.0 +- weighted_linear_score: 0.692308 +- hybrid_score: 0.8; H: 0.5; H_label: moderately homogeneous +- evenness_score: 0.729574; classification: m2-dominant; mid evenness; uniform stacking +- distance_counts: {'1': 3, '2': 2, '3': 1} +- adj_counts: {'1': 3} +- Note: Adjacent regularity exceeds pairwise concentration under current semantics. + +## inversion_same_interval_multiset + +**Description:** Inversion of major_triad_close around E4: E4 G#4 C#4. + +- Notes: `[['E', 0.0, 4], ['G', 1.0, 4], ['C', 1.0, 4]]` +- Cardinality: 3; unique pitches: 3 +- pair_score: 0.333333; chain_score: 0.5 +- weighted_linear_score: 0.4 +- hybrid_score: 0.433333; H: 0.333333; H_label: moderately heterogeneous +- evenness_score: 0.528321; classification: no dominant interval; mid evenness; irregular stacking +- distance_counts: {'4': 1, '3': 1, '7': 1} +- adj_counts: {'3': 1, '4': 1} +- Note: Adjacent regularity exceeds pairwise concentration under current semantics. + +## EDO_24_microtonal_regular + +**Description:** Quarter-tone chromatic segment on 24-EDO grid (bin_cents=50). + +- Notes: `[['C', 0.0, 4], ['C', 0.5, 4], ['D', 0.0, 4], ['D', 0.5, 4]]` +- Cardinality: 4; unique pitches: 4 +- pair_score: 0.333333; chain_score: 0.666667 +- weighted_linear_score: 0.461538 +- hybrid_score: 0.533333; H: 0.333333; H_label: moderately heterogeneous +- evenness_score: 0.742098; classification: no dominant interval; mid evenness; predominantly regular stacking +- distance_counts: {'1': 2, '4': 2, '5': 1, '3': 1} +- adj_counts: {'1': 2, '3': 1} +- Note: Adjacent regularity exceeds pairwise concentration under current semantics. + +## EDO_bin_change_sensitivity + +**Description:** Same four semitones analysed at bin_cents=100 vs 50 (modelling choice). + +### bin_cents=100 +- H: 0.5; pair_score: 0.5; chain_score: 1.0 +- distance_counts: {'1': 3, '2': 2, '3': 1} + +### bin_cents=50 +- H: 0.5; pair_score: 0.5; chain_score: 1.0 +- distance_counts: {'2': 3, '4': 2, '6': 1} + +## repeated_pitch_density_not_homogeneity + +**Description:** C4 C4 C4 G4: duplicate pitches preserved in manual aggregate path. + +- Notes: `[['C', 0.0, 4], ['C', 0.0, 4], ['C', 0.0, 4], ['G', 0.0, 4]]` +- Cardinality: 4; unique pitches: 2 +- pair_score: 0.5; chain_score: 0.666667 +- weighted_linear_score: 0.576923 +- hybrid_score: 0.6; H: 0.5; H_label: moderately homogeneous +- evenness_score: 0.333333; classification: P1-dominant; mid evenness; predominantly regular stacking +- distance_counts: {'0': 3, '7': 3} +- adj_counts: {'0': 2, '7': 1} +- Note: Adjacent regularity exceeds pairwise concentration under current semantics. + +## passage_changing_interval_field + +**Description:** Three verticalities: chromatic cluster, whole-tone segment, diminished seventh. + +### Slice 1: chromatic_cluster +- Notes: `[['C', 0.0, 4], ['C', 1.0, 4], ['D', 0.0, 4], ['D', 1.0, 4]]` +- H: 0.5; pair_score: 0.5; chain_score: 1.0 +- hybrid_score: 0.8; evenness: 0.729574 +- Note: Adjacent regularity exceeds pairwise concentration under current semantics. + +### Slice 2: whole_tone_segment +- Notes: `[['C', 0.0, 4], ['D', 0.0, 4], ['E', 0.0, 4], ['F', 1.0, 4]]` +- H: 0.5; pair_score: 0.5; chain_score: 1.0 +- hybrid_score: 0.8; evenness: 0.519759 +- Note: Adjacent regularity exceeds pairwise concentration under current semantics. + +### Slice 3: diminished_seventh +- Notes: `[['C', 0.0, 4], ['E', -1.0, 4], ['G', -1.0, 4], ['A', 0.0, 4]]` +- H: 0.5; pair_score: 0.5; chain_score: 1.0 +- hybrid_score: 0.8; evenness: 0.439247 +- Note: Adjacent regularity exceeds pairwise concentration under current semantics. + +- interval_profile_changes: True + diff --git a/corpus/scripts/create_interval_homogeneity_regression_fixtures.py b/corpus/scripts/create_interval_homogeneity_regression_fixtures.py new file mode 100644 index 0000000..7b42efb --- /dev/null +++ b/corpus/scripts/create_interval_homogeneity_regression_fixtures.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +"""Generate deterministic JSON fixtures for interval homogeneity regression (phase 1).""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Dict, List, Tuple + +ROOT = Path(__file__).resolve().parents[2] +FIXTURE_DIR = ROOT / "corpus" / "fixtures" / "interval_homogeneity_regression" + +NoteTuple = Tuple[str, float, int] + +DEFAULT_ANALYSIS: Dict[str, Any] = { + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.60, + "intervallic_headline_mode": "pairwise_intervallic_concentration", +} + +FIXTURES: Dict[str, Dict[str, Any]] = { + "unison_dyad": { + "description": "Degenerate dyad: duplicate C4. Two vertical events, one unique pitch height.", + "tags": ["dyad", "degenerate", "duplicate"], + "notes": [("C", 0.0, 4), ("C", 0.0, 4)], + "qualitative": { + "vertical_note_count": 2, + "unique_pitch_heights": 1, + "single_interval_concentration": True, + "not_perceptual_consonance": True, + }, + }, + "single_interval_dyad_perfect_fifth": { + "description": "Dyad C4–G4: one pairwise interval type (P5 on 12-TET grid).", + "tags": ["dyad", "single_interval"], + "notes": [("C", 0.0, 4), ("G", 0.0, 4)], + "qualitative": {"single_interval_concentration": True, "not_perceptual_consonance": True}, + }, + "minor_second_dyad": { + "description": "Dyad C4–C#4: single m2 interval; high concentration does not imply consonance.", + "tags": ["dyad", "single_interval", "dissonant_spelling"], + "notes": [("C", 0.0, 4), ("C", 1.0, 4)], + "qualitative": {"single_interval_concentration": True, "not_perceptual_consonance": True}, + }, + "chromatic_cluster_four": { + "description": "Four consecutive semitones C4–D#4: regular adjacent spacing, diverse pairwise set.", + "tags": ["cluster", "adjacent_vs_pairwise"], + "notes": [("C", 0.0, 4), ("C", 1.0, 4), ("D", 0.0, 4), ("D", 1.0, 4)], + "qualitative": {"chain_gt_pair": True}, + }, + "whole_tone_segment_four": { + "description": "Whole-tone segment C4–F#4: uniform adjacent M2, broader pairwise distances.", + "tags": ["whole_tone", "adjacent_vs_pairwise"], + "notes": [("C", 0.0, 4), ("D", 0.0, 4), ("E", 0.0, 4), ("F", 1.0, 4)], + "qualitative": {"chain_gt_pair": True}, + }, + "stacked_fourths": { + "description": "Quartal stack C4–Eb5: repeated P4 adjacent intervals.", + "tags": ["quartal", "regular_adjacent"], + "notes": [("C", 0.0, 4), ("F", 0.0, 4), ("B", -1.0, 4), ("E", -1.0, 5)], + "qualitative": {"chain_gt_pair": True}, + }, + "stacked_fifths": { + "description": "Fifths stack C3–A4: repeated P5 adjacent intervals.", + "tags": ["quintal", "regular_adjacent"], + "notes": [("C", 0.0, 3), ("G", 0.0, 3), ("D", 0.0, 4), ("A", 0.0, 4)], + "qualitative": {"chain_gt_pair": True}, + }, + "major_triad_close": { + "description": "Closed major triad C4–E4–G4: three distinct pairwise interval types.", + "tags": ["triad", "irregular_pairwise"], + "notes": [("C", 0.0, 4), ("E", 0.0, 4), ("G", 0.0, 4)], + "qualitative": {"compare_with": "augmented_triad_symmetric"}, + }, + "augmented_triad_symmetric": { + "description": "Augmented triad C4–E4–G#4: equal major-third division in 12-TET.", + "tags": ["triad", "symmetric"], + "notes": [("C", 0.0, 4), ("E", 0.0, 4), ("G", 1.0, 4)], + "qualitative": {"stronger_than": "major_triad_close"}, + }, + "diminished_seventh_symmetric": { + "description": "Diminished seventh C4–A4: symmetric minor-third tiling.", + "tags": ["seventh", "symmetric"], + "notes": [("C", 0.0, 4), ("E", -1.0, 4), ("G", -1.0, 4), ("A", 0.0, 4)], + "qualitative": {"stronger_than": "dominant_seventh_irregular"}, + }, + "dominant_seventh_irregular": { + "description": "Dominant seventh C4–Bb4: same cardinality as dim7, less regular pairwise profile.", + "tags": ["seventh", "irregular"], + "notes": [("C", 0.0, 4), ("E", 0.0, 4), ("G", 0.0, 4), ("B", -1.0, 4)], + "qualitative": {"same_cardinality_as": "diminished_seventh_symmetric"}, + }, + "same_cardinality_different_distribution_A": { + "description": "Four-note diatonic cluster C4–F4: compact scalar / adjacent regularity.", + "tags": ["cardinality_control", "compact"], + "notes": [("C", 0.0, 4), ("D", 0.0, 4), ("E", 0.0, 4), ("F", 0.0, 4)], + "qualitative": {"cardinality": 4}, + }, + "same_cardinality_different_distribution_B": { + "description": "Four-note wide/symmetric C4–C5: same cardinality, different interval distribution.", + "tags": ["cardinality_control", "wide"], + "notes": [("C", 0.0, 4), ("E", 0.0, 4), ("G", 1.0, 4), ("C", 0.0, 5)], + "qualitative": {"cardinality": 4, "differs_from": "same_cardinality_different_distribution_A"}, + }, + "octave_duplication_case": { + "description": "C4 C5 G4: octave duplication; model uses distinct pitch heights (no pc collapse).", + "tags": ["octave", "register"], + "notes": [("C", 0.0, 4), ("C", 0.0, 5), ("G", 0.0, 4)], + "qualitative": {"octave_equivalence_applied": False}, + }, + "transposed_same_structure": { + "description": "chromatic_cluster_four transposed up major second (D4–F#4).", + "tags": ["metamorphic", "transposition"], + "notes": [("D", 0.0, 4), ("D", 1.0, 4), ("E", 0.0, 4), ("E", 1.0, 4)], + "reference_fixture": "chromatic_cluster_four", + "qualitative": {"transposition_invariant": True}, + }, + "inversion_same_interval_multiset": { + "description": "Inversion of major_triad_close around E4: E4 G#4 C#4.", + "tags": ["metamorphic", "inversion"], + "notes": [("E", 0.0, 4), ("G", 1.0, 4), ("C", 1.0, 4)], + "reference_fixture": "major_triad_close", + "qualitative": {"interval_multiset_preserved": True}, + }, + "EDO_24_microtonal_regular": { + "description": "Quarter-tone chromatic segment on 24-EDO grid (bin_cents=50).", + "tags": ["microtonal", "edo24"], + "notes": [("C", 0.0, 4), ("C", 0.5, 4), ("D", 0.0, 4), ("D", 0.5, 4)], + "analysis_override": {"bin_cents": 50, "edo": 24}, + "qualitative": {"regular_adjacent_spacing": True}, + }, + "EDO_bin_change_sensitivity": { + "description": "Same four semitones analysed at bin_cents=100 vs 50 (modelling choice).", + "tags": ["edo", "binning"], + "notes": [("C", 0.0, 4), ("C", 1.0, 4), ("D", 0.0, 4), ("D", 1.0, 4)], + "bin_variants": [100, 50], + "qualitative": {"binning_is_modelling_choice": True}, + }, + "repeated_pitch_density_not_homogeneity": { + "description": "C4 C4 C4 G4: duplicate pitches preserved in manual aggregate path.", + "tags": ["duplicate", "density"], + "notes": [("C", 0.0, 4), ("C", 0.0, 4), ("C", 0.0, 4), ("G", 0.0, 4)], + "qualitative": { + "duplicates_preserved_in_raw_metrics": True, + "dedupe_collapses_to_dyad": True, + }, + }, + "passage_changing_interval_field": { + "description": "Three verticalities: chromatic cluster, whole-tone segment, diminished seventh.", + "tags": ["passage", "multi_slice"], + "slices": [ + { + "slice_id": 1, + "label": "chromatic_cluster", + "notes": [("C", 0.0, 4), ("C", 1.0, 4), ("D", 0.0, 4), ("D", 1.0, 4)], + }, + { + "slice_id": 2, + "label": "whole_tone_segment", + "notes": [("C", 0.0, 4), ("D", 0.0, 4), ("E", 0.0, 4), ("F", 1.0, 4)], + }, + { + "slice_id": 3, + "label": "diminished_seventh", + "notes": [("C", 0.0, 4), ("E", -1.0, 4), ("G", -1.0, 4), ("A", 0.0, 4)], + }, + ], + "qualitative": {"changing_interval_profile": True}, + }, +} + + +def _serialize_notes(notes: List[NoteTuple]) -> List[List[Any]]: + return [[letter, alter, octave] for letter, alter, octave in notes] + + +def _fixture_payload(fixture_id: str, spec: Dict[str, Any]) -> Dict[str, Any]: + payload: Dict[str, Any] = { + "schema_version": 1, + "fixture_id": fixture_id, + "description": spec["description"], + "tags": spec["tags"], + "analysis": {**DEFAULT_ANALYSIS, **spec.get("analysis_override", {})}, + "qualitative": spec.get("qualitative", {}), + } + if "notes" in spec: + payload["notes"] = _serialize_notes(spec["notes"]) + if "slices" in spec: + payload["slices"] = [ + { + "slice_id": s["slice_id"], + "label": s["label"], + "notes": _serialize_notes(s["notes"]), + } + for s in spec["slices"] + ] + if "reference_fixture" in spec: + payload["reference_fixture"] = spec["reference_fixture"] + if "bin_variants" in spec: + payload["bin_variants"] = spec["bin_variants"] + return payload + + +def main() -> int: + FIXTURE_DIR.mkdir(parents=True, exist_ok=True) + manifest_ids: List[str] = [] + for fixture_id, spec in FIXTURES.items(): + path = FIXTURE_DIR / f"{fixture_id}.json" + path.write_text( + json.dumps(_fixture_payload(fixture_id, spec), indent=2, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + manifest_ids.append(fixture_id) + + manifest = { + "schema_version": 1, + "suite": "interval_homogeneity_regression", + "phase": 1, + "fixture_count": len(manifest_ids), + "fixtures": manifest_ids, + "default_analysis": DEFAULT_ANALYSIS, + "notes": ( + "Qualitative/metamorphic regression only. No locked scalar golden values in phase 1." + ), + } + (FIXTURE_DIR / "manifest.json").write_text( + json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + print(f"Wrote {len(manifest_ids)} fixtures to {FIXTURE_DIR}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/corpus/scripts/inspect_interval_homogeneity_regression.py b/corpus/scripts/inspect_interval_homogeneity_regression.py new file mode 100644 index 0000000..ee2a7c5 --- /dev/null +++ b/corpus/scripts/inspect_interval_homogeneity_regression.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +"""Exploratory inspection report for interval homogeneity regression fixtures (phase 1).""" + +from __future__ import annotations + +import json +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Dict, List, Sequence, Tuple + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from iav.analysis_enums import IntervallicHeadlineMode +from iav.canonical_corpus import analyze_sonority, notes_from_json_rows +from iav.interval_analysis_core import dedupe_notes_by_midi, metrics_for_notes +from iav.symbolic_profile import passage_delta_rows +from iav.vertical_cardinality import vertical_cardinality_for_notes + +FIXTURE_DIR = ROOT / "corpus" / "fixtures" / "interval_homogeneity_regression" +REPORT_DIR = ROOT / "corpus" / "reports" +REPORT_MD = REPORT_DIR / "interval_homogeneity_regression_inspection.md" +REPORT_JSON = REPORT_DIR / "interval_homogeneity_regression_inspection.json" + + +def _load_manifest() -> Dict[str, Any]: + return json.loads((FIXTURE_DIR / "manifest.json").read_text(encoding="utf-8")) + + +def _load_fixture(fixture_id: str) -> Dict[str, Any]: + return json.loads((FIXTURE_DIR / f"{fixture_id}.json").read_text(encoding="utf-8")) + + +def _analysis_params(payload: Dict[str, Any]) -> Dict[str, Any]: + analysis = payload.get("analysis", {}) + return { + "dominance_threshold": float(analysis.get("dominance_threshold", 0.35)), + "even_high": float(analysis.get("even_high", 0.8)), + "even_low": float(analysis.get("even_low", 0.3)), + "hybrid_alpha": float(analysis.get("hybrid_alpha", 0.6)), + "homogeneity_method": str(analysis.get("homogeneity_method", "dominance")), + "bin_cents": int(analysis.get("bin_cents", 100)), + "chain_threshold": float(analysis.get("chain_threshold", 0.60)), + } + + +def _metrics_bundle( + notes: Sequence[Tuple[str, float, int]], + params: Dict[str, Any], +) -> Dict[str, Any]: + m = metrics_for_notes( + notes, + params["dominance_threshold"], + params["even_high"], + params["even_low"], + params["hybrid_alpha"], + params["homogeneity_method"], + bin_cents=params["bin_cents"], + chain_threshold=params["chain_threshold"], + intervallic_headline_mode=IntervallicHeadlineMode.PAIRWISE, + ) + analyzed = analyze_sonority( + notes, + hybrid_alpha=params["hybrid_alpha"], + homogeneity_method=params["homogeneity_method"], + bin_cents=params["bin_cents"], + dominance_threshold=params["dominance_threshold"], + even_high=params["even_high"], + even_low=params["even_low"], + chain_threshold=params["chain_threshold"], + ) + card = vertical_cardinality_for_notes(notes, bin_cents=params["bin_cents"], edo=12) + return { + "cardinality": len(notes), + "vertical_note_count": card["vertical_note_count"], + "vertical_unique_pitch_count": card["vertical_unique_pitch_count"], + "pair_score": round(float(m["pair_score"]), 6), + "chain_score": round(float(m["chain_score"]), 6), + "weighted_linear_score": round(float(m["weighted_linear_score"]), 6), + "weighted_quadratic_score": round(float(m["weighted_quadratic_score"]), 6), + "hybrid_score": round(float(analyzed["hybrid_score"]), 6), + "H": round(float(m["H"]), 6), + "H_label": str(m["H_label"]), + "evenness_score": round(float(m["evenness_score"]), 6), + "type_score": round(float(m["type_score"]), 6), + "classification": str(m["classification"]), + "distance_counts": {str(k): v for k, v in m["distance_counts"].items()}, + "adj_counts": {str(k): v for k, v in m["adj_counts"].items()}, + "dominant_pairwise_interval": analyzed.get("dominant_pairwise_interval"), + "dominant_adjacent_interval": analyzed.get("dominant_adjacent_interval"), + "interpretation_note": _interpretation_note(payload_id="", m=m, analyzed=analyzed), + } + + +def _interpretation_note(*, payload_id: str, m: Dict[str, Any], analyzed: Dict[str, Any]) -> str: + pair = float(m["pair_score"]) + chain = float(m["chain_score"]) + if pair >= 0.99 and chain >= 0.99: + return "Single-interval or maximally concentrated dyad/degenerate case (not consonance validation)." + if chain > pair + 0.05: + return "Adjacent regularity exceeds pairwise concentration under current semantics." + if pair > chain + 0.05: + return "Pairwise concentration exceeds adjacent regularity." + return "Mixed interval distribution; read H only with full interval profile context." + + +def _inspect_single_fixture(fixture_id: str) -> Dict[str, Any]: + payload = _load_fixture(fixture_id) + params = _analysis_params(payload) + row: Dict[str, Any] = { + "fixture": fixture_id, + "description": payload.get("description"), + "tags": payload.get("tags", []), + "analysis": params, + } + + if "slices" in payload: + slice_rows: List[Dict[str, Any]] = [] + summary_rows: List[Dict[str, Any]] = [] + for sl in payload["slices"]: + notes = notes_from_json_rows(sl["notes"]) + metrics = _metrics_bundle(notes, params) + metrics["slice_id"] = sl["slice_id"] + metrics["label"] = sl["label"] + metrics["notes"] = sl["notes"] + metrics["interpretation_note"] = _interpretation_note( + payload_id=fixture_id, m=metrics, analyzed={"hybrid_score": metrics["hybrid_score"]} + ) + slice_rows.append(metrics) + summary_rows.append( + { + "Slice": sl["slice_id"], + "Label": sl["label"], + "H (interval dominance)": metrics["H"], + } + ) + row["slices"] = slice_rows + row["passage_delta_rows"] = passage_delta_rows(summary_rows) + row["interval_profile_changes"] = len({json.dumps(s["distance_counts"]) for s in slice_rows}) > 1 + return row + + if fixture_id == "EDO_bin_change_sensitivity": + notes = notes_from_json_rows(payload["notes"]) + variants: List[Dict[str, Any]] = [] + for bin_cents in payload.get("bin_variants", [100, 50]): + p = {**params, "bin_cents": int(bin_cents)} + metrics = _metrics_bundle(notes, p) + metrics["bin_cents"] = bin_cents + metrics["interpretation_note"] = ( + f"Observed at bin_cents={bin_cents}; binning is a modelling choice, not tuning." + ) + variants.append(metrics) + row["bin_variants"] = variants + row["labels_or_metrics_differ"] = any( + variants[0]["H"] != v["H"] or variants[0]["distance_counts"] != v["distance_counts"] + for v in variants[1:] + ) + return row + + notes = notes_from_json_rows(payload["notes"]) + row["notes"] = payload["notes"] + metrics = _metrics_bundle(notes, params) + metrics["interpretation_note"] = _interpretation_note( + payload_id=fixture_id, + m=metrics, + analyzed={"hybrid_score": metrics["hybrid_score"]}, + ) + row.update(metrics) + + if fixture_id == "repeated_pitch_density_not_homogeneity": + deduped = dedupe_notes_by_midi(notes, bin_cents=params["bin_cents"]) + row["deduped_note_count"] = len(deduped) + row["deduped_metrics"] = _metrics_bundle(deduped, params) + + if payload.get("reference_fixture"): + ref_notes = notes_from_json_rows(_load_fixture(payload["reference_fixture"])["notes"]) + ref_metrics = _metrics_bundle(ref_notes, params) + row["reference_fixture"] = payload["reference_fixture"] + row["reference_metrics"] = ref_metrics + + return row + + +def _render_markdown(rows: List[Dict[str, Any]], generated_at: str) -> str: + lines = [ + "# Interval homogeneity regression — exploratory inspection (phase 1)", + "", + f"Generated: {generated_at}", + "", + "These values are **exploratory** only. Do not promote them to strict golden references.", + "", + ] + for row in rows: + lines.append(f"## {row['fixture']}") + lines.append("") + lines.append(f"**Description:** {row.get('description', '')}") + lines.append("") + if "slices" in row: + for sl in row["slices"]: + lines.append(f"### Slice {sl['slice_id']}: {sl['label']}") + lines.append(f"- Notes: `{sl['notes']}`") + lines.append(f"- H: {sl['H']}; pair_score: {sl['pair_score']}; chain_score: {sl['chain_score']}") + lines.append(f"- hybrid_score: {sl['hybrid_score']}; evenness: {sl['evenness_score']}") + lines.append(f"- Note: {sl['interpretation_note']}") + lines.append("") + lines.append(f"- interval_profile_changes: {row.get('interval_profile_changes')}") + lines.append("") + continue + if "bin_variants" in row: + for variant in row["bin_variants"]: + lines.append(f"### bin_cents={variant['bin_cents']}") + lines.append(f"- H: {variant['H']}; pair_score: {variant['pair_score']}; chain_score: {variant['chain_score']}") + lines.append(f"- distance_counts: {variant['distance_counts']}") + lines.append("") + continue + lines.append(f"- Notes: `{row.get('notes')}`") + lines.append(f"- Cardinality: {row.get('cardinality')}; unique pitches: {row.get('vertical_unique_pitch_count')}") + lines.append(f"- pair_score: {row.get('pair_score')}; chain_score: {row.get('chain_score')}") + lines.append(f"- weighted_linear_score: {row.get('weighted_linear_score')}") + lines.append(f"- hybrid_score: {row.get('hybrid_score')}; H: {row.get('H')}; H_label: {row.get('H_label')}") + lines.append(f"- evenness_score: {row.get('evenness_score')}; classification: {row.get('classification')}") + lines.append(f"- distance_counts: {row.get('distance_counts')}") + lines.append(f"- adj_counts: {row.get('adj_counts')}") + lines.append(f"- Note: {row.get('interpretation_note')}") + lines.append("") + return "\n".join(lines) + "\n" + + +def main() -> int: + manifest = _load_manifest() + fixture_ids: List[str] = list(manifest["fixtures"]) + rows = [_inspect_single_fixture(fid) for fid in fixture_ids] + generated_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + REPORT_DIR.mkdir(parents=True, exist_ok=True) + payload = { + "generated_at": generated_at, + "suite": "interval_homogeneity_regression", + "phase": 1, + "fixture_count": len(rows), + "fixtures": rows, + } + REPORT_JSON.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + REPORT_MD.write_text(_render_markdown(rows, generated_at), encoding="utf-8") + print(f"Wrote {REPORT_MD}") + print(f"Wrote {REPORT_JSON}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docs/INTERVAL_HOMOGENEITY_REGRESSION_FIXTURES.md b/docs/INTERVAL_HOMOGENEITY_REGRESSION_FIXTURES.md new file mode 100644 index 0000000..87afc7b --- /dev/null +++ b/docs/INTERVAL_HOMOGENEITY_REGRESSION_FIXTURES.md @@ -0,0 +1,230 @@ +# Interval homogeneity regression fixtures (phase 1) + +Controlled symbolic fixtures and qualitative/metamorphic tests for **Interval Homogeneity Analyser** (`iav`). Phase 1 validates **musicological semantics of interval distribution concentration** — not aesthetic judgment, not acoustic consonance, and not perceptual dissonance. + +## Purpose + +This suite guards against silent drift in: + +- pairwise vs adjacent vs proximity-weighted vs hybrid homogeneity semantics; +- transposition and inversion invariance of distance-based metrics; +- cardinality vs interval-distribution distinctions; +- duplicate-pitch handling in manual aggregates; +- EDO / binning modelling choices; +- multi-slice passage profiles and ΔH along symbolic time. + +**Phase 1 uses qualitative and metamorphic assertions only.** Scalar outputs are inspected in `corpus/reports/interval_homogeneity_regression_inspection.md` but are **not** locked as golden references yet. + +## Symbolic / notational status + +All fixtures are **symbolic pitch aggregates** represented as `(letter, alter, octave)` tuples or JSON-encoded equivalents. They describe **notated pitch-height relationships** on a selectable equal-division grid (`bin_cents`). They do **not** represent audio waveforms, spectra, or listener responses. + +## Scope disclaimer + +| In scope | Out of scope | +|----------|--------------| +| Interval-type concentration on an EDO grid | Acoustic consonance / roughness | +| Adjacent regularity after pitch-height sort | Psychoacoustic dissonance | +| Pairwise distance histogram shape | Harmonic-function or tonal interpretation | +| Metamorphic invariance (transposition, inversion multiset) | Performance timing or rubato | +| Passage-level ΔH between verticalities | Perceptual “beauty” or preference | + +## Metric semantics (distinctions) + +| Metric / field | Meaning in this suite | +|----------------|----------------------| +| `pair_score` | Dominance (or entropy) concentration of **all unordered pairwise absolute pitch distances** | +| `chain_score` | Same concentration measure on **adjacent intervals** after sorting by pitch height (also called adjacent regularity) | +| `weighted_linear_score` | Proximity-weighted pairs with weight **1/|i−j|** | +| `weighted_quadratic_score` | Proximity-weighted pairs with weight **1/|i−j|²** | +| `H` | Headline homogeneity; under default regression settings follows **pairwise** concentration | +| `hybrid_score` | α·adjacent + (1−α)·pairwise with α = 0.6 in default analysis block | +| `evenness_score` | Support-bounded Shannon evenness of the pairwise distance histogram | +| `distance_counts` / `adj_counts` | Empirical interval-distance histograms (modelling outputs) | + +**Interval concentration ≠ consonance.** A minor-second dyad and a perfect-fifth dyad can both show **maximal single-interval concentration** because each has only one pairwise distance type. That result describes **distribution shape**, not pleasantness. + +**Do not read H as an isolated scalar truth.** Always interpret alongside `distance_counts`, `adj_counts`, and the analysis configuration (`bin_cents`, `homogeneity_method`, headline mode). + +## Dyad degeneracy + +`unison_dyad` documents a **degenerate case**: two vertical events at the same pitch height. Vertical cardinality may be 2 while unique pitch content is 1. Pairwise concentration can be maximal by construction (one distance type: unison). This is **not** perceptual consonance validation. + +## Cardinality vs interval distribution + +`same_cardinality_different_distribution_A` and `same_cardinality_different_distribution_B` share note count (4) but differ in interval-distance profiles. Homogeneity metrics must **not** be inferred from cardinality alone. + +## Duplicate pitches + +`repeated_pitch_density_not_homogeneity` documents current manual-path behaviour: + +- `metrics_for_notes` on the **raw** note list **preserves** duplicate pitch heights in interval counting. +- `dedupe_notes_by_midi` (used in MusicXML batch paths) **collapses** duplicate heights before analysis. + +Tests assert the observed implementation; they do not invent deduplication policy. + +## Octave equivalence + +`octave_duplication_case` (C4, C5, G4) clarifies that the model uses **distinct pitch heights**; octave-related pitches are **not** collapsed to pitch class in homogeneity metrics. + +## EDO / binning as modelling choice + +`EDO_24_microtonal_regular` uses quarter-tone spellings with `bin_cents=50` (24-EDO). +`EDO_bin_change_sensitivity` analyses the same chromatic segment at `bin_cents=100` vs `50`. Labels and H may change; bin width is a **modelling choice**, not performance tuning. + +## File locations + +| Path | Role | +|------|------| +| `corpus/fixtures/interval_homogeneity_regression/*.json` | Fixture definitions | +| `corpus/fixtures/interval_homogeneity_regression/manifest.json` | Fixture index | +| `corpus/scripts/create_interval_homogeneity_regression_fixtures.py` | Deterministic generator | +| `corpus/scripts/inspect_interval_homogeneity_regression.py` | Exploratory inspection report | +| `corpus/reports/interval_homogeneity_regression_inspection.md` | Human-readable metrics snapshot | +| `corpus/reports/interval_homogeneity_regression_inspection.json` | Machine-readable inspection | +| `tests/test_interval_homogeneity_regression_fixtures.py` | Qualitative pytest suite | + +Regenerate fixtures: + +```bash +python corpus/scripts/create_interval_homogeneity_regression_fixtures.py +python corpus/scripts/inspect_interval_homogeneity_regression.py +``` + +## Default analysis block (fixtures) + +```json +{ + "homogeneity_method": "dominance", + "bin_cents": 100, + "hybrid_alpha": 0.6, + "dominance_threshold": 0.35, + "even_high": 0.8, + "even_low": 0.3, + "chain_threshold": 0.60, + "intervallic_headline_mode": "pairwise_intervallic_concentration" +} +``` + +## Fixture catalogue + +### 1. `unison_dyad` + +- **Notes:** C4, C4 +- **Expected (qualitative):** vertical count 2, unique pitch height 1; maximally concentrated single distance type; degenerate/simple case. +- **Not:** perceptual consonance validation. + +### 2. `single_interval_dyad_perfect_fifth` + +- **Notes:** C4, G4 +- **Expected:** one pairwise interval type; high/maximal `pair_score` by construction. +- **Not:** acoustic consonance proof. + +### 3. `minor_second_dyad` + +- **Notes:** C4, C#4 +- **Expected:** same single-interval concentration logic as the fifth dyad. +- **Demonstrates:** homogeneity ≠ consonance. + +### 4. `chromatic_cluster_four` + +- **Notes:** C4, C#4, D4, D#4 +- **Expected:** regular adjacent semitones; more diverse pairwise set; `chain_score` > `pair_score`. + +### 5. `whole_tone_segment_four` + +- **Notes:** C4, D4, E4, F#4 +- **Expected:** uniform whole-tone adjacent spacing; broader pairwise distances; adjacent vs pairwise distinction. + +### 6. `stacked_fourths` + +- **Notes:** C4, F4, Bb4, Eb5 +- **Expected:** repeated P4 adjacent intervals; wider compound pairwise relations. + +### 7. `stacked_fifths` + +- **Notes:** C3, G3, D4, A4 +- **Expected:** repeated P5 adjacent intervals; compare interval-type concentration with `stacked_fourths`. + +### 8. `major_triad_close` + +- **Notes:** C4, E4, G4 +- **Expected:** multiple pairwise interval types; not maximally homogeneous; compare with `augmented_triad_symmetric`. + +### 9. `augmented_triad_symmetric` + +- **Notes:** C4, E4, G#4 +- **Expected:** stronger interval-type regularity than major triad under pairwise logic. +- **Not:** perceptual equivalence claim. + +### 10. `diminished_seventh_symmetric` + +- **Notes:** C4, Eb4, Gb4, A4 +- **Expected:** symmetric equal minor-third division; high adjacent regularity. + +### 11. `dominant_seventh_irregular` + +- **Notes:** C4, E4, G4, Bb4 +- **Expected:** same cardinality as dim7; more diverse pairwise profile. + +### 12. `same_cardinality_different_distribution_A` + +- **Notes:** C4, D4, E4, F4 +- **Expected:** compact scalar cluster; distinct distribution from fixture B. + +### 13. `same_cardinality_different_distribution_B` + +- **Notes:** C4, E4, G#4, C5 +- **Expected:** same cardinality (4); different interval distribution. + +### 14. `octave_duplication_case` + +- **Notes:** C4, C5, G4 +- **Expected:** octave-related heights treated as distinct; affects pairwise histogram. + +### 15. `transposed_same_structure` + +- **Notes:** D4, D#4, E4, F#4 (transposition of `chromatic_cluster_four`) +- **Expected:** `pair_score`, `chain_score`, `H`, `hybrid_score` invariant within tolerance. + +### 16. `inversion_same_interval_multiset` + +- **Notes:** E4, G#4, C#4 (inversional counterpart of `major_triad_close` around E4) +- **Expected:** pairwise distance multiset preserved; distance-based homogeneity stable. + +### 17. `EDO_24_microtonal_regular` + +- **Notes:** C4, C+0.5, D4, D+0.5 with `bin_cents=50` +- **Expected:** no crash; coherent quarter-tone grid labels; stable qualitative relations only. + +### 18. `EDO_bin_change_sensitivity` + +- **Notes:** chromatic cluster; analysed at `bin_cents` 100 and 50 +- **Expected:** labels and/or metrics may differ; documents binning as modelling choice. + +### 19. `repeated_pitch_density_not_homogeneity` + +- **Notes:** C4, C4, C4, G4 +- **Expected:** raw aggregate preserves duplicates; dedupe path collapses to C4+G4 dyad. + +### 20. `passage_changing_interval_field` + +- **Slices:** chromatic cluster → whole-tone segment → diminished seventh +- **Expected:** multiple passage rows; changing interval profiles; ΔH column populated where implemented. + +## Qualitative-only expectations (phase 1) + +The following are asserted qualitatively in tests, **not** as frozen scalars: + +- dyad single-interval concentration (fifth and minor second); +- `chain_score` > `pair_score` for chromatic / whole-tone tetrachords; +- symmetric > irregular chord concentration ordering (dim7 vs dom7; aug vs major); +- transposition invariance of distance metrics; +- inversion multiset preservation; +- duplicate-pitch policy (raw vs deduped); +- passage interval-profile change across slices; +- documentation completeness for every fixture id. + +## Phase 2 (not in scope yet) + +Promoting selected inspection values to frozen golden JSON (as in `iav/data/canonical_sonorities.json`) requires an explicit decision record and licensed-corpus review. Do not copy phase-1 inspection numbers into strict regression without that review. diff --git a/tests/test_interval_homogeneity_regression_fixtures.py b/tests/test_interval_homogeneity_regression_fixtures.py new file mode 100644 index 0000000..04f8315 --- /dev/null +++ b/tests/test_interval_homogeneity_regression_fixtures.py @@ -0,0 +1,308 @@ +""" +Qualitative and metamorphic invariants for interval homogeneity regression (phase 1). + +No locked golden scalar values — structural and musicological-semantic checks only. +""" + +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Any, Dict, List, Sequence, Tuple + +import pytest + +from iav.analysis_enums import IntervallicHeadlineMode +from iav.canonical_corpus import analyze_sonority, notes_from_json_rows +from iav.interval_analysis_core import dedupe_notes_by_midi, metrics_for_notes +from iav.symbolic_profile import passage_delta_rows +from iav.vertical_cardinality import vertical_cardinality_for_notes + +ROOT = Path(__file__).resolve().parents[1] +FIXTURE_DIR = ROOT / "corpus" / "fixtures" / "interval_homogeneity_regression" +DOCS_PATH = ROOT / "docs" / "INTERVAL_HOMOGENEITY_REGRESSION_FIXTURES.md" + +NoteTuple = Tuple[str, float, int] + +SINGLE_NOTE_FIXTURES = { + "unison_dyad", + "single_interval_dyad_perfect_fifth", + "minor_second_dyad", +} + +PASSAGE_FIXTURE = "passage_changing_interval_field" + +ALL_FIXTURE_IDS = [ + "unison_dyad", + "single_interval_dyad_perfect_fifth", + "minor_second_dyad", + "chromatic_cluster_four", + "whole_tone_segment_four", + "stacked_fourths", + "stacked_fifths", + "major_triad_close", + "augmented_triad_symmetric", + "diminished_seventh_symmetric", + "dominant_seventh_irregular", + "same_cardinality_different_distribution_A", + "same_cardinality_different_distribution_B", + "octave_duplication_case", + "transposed_same_structure", + "inversion_same_interval_multiset", + "EDO_24_microtonal_regular", + "EDO_bin_change_sensitivity", + "repeated_pitch_density_not_homogeneity", + PASSAGE_FIXTURE, +] + + +def _fixture_path(fixture_id: str) -> Path: + path = FIXTURE_DIR / f"{fixture_id}.json" + if not path.exists(): + pytest.skip( + f"fixture missing: {path.name} — run create_interval_homogeneity_regression_fixtures.py" + ) + return path + + +def _load_fixture(fixture_id: str) -> Dict[str, Any]: + return json.loads(_fixture_path(fixture_id).read_text(encoding="utf-8")) + + +def _analysis_params(payload: Dict[str, Any]) -> Dict[str, Any]: + analysis = payload.get("analysis", {}) + return { + "dominance_threshold": float(analysis.get("dominance_threshold", 0.35)), + "even_high": float(analysis.get("even_high", 0.8)), + "even_low": float(analysis.get("even_low", 0.3)), + "hybrid_alpha": float(analysis.get("hybrid_alpha", 0.6)), + "homogeneity_method": str(analysis.get("homogeneity_method", "dominance")), + "bin_cents": int(analysis.get("bin_cents", 100)), + "chain_threshold": float(analysis.get("chain_threshold", 0.60)), + } + + +def _metrics( + notes: Sequence[NoteTuple], + params: Dict[str, Any], +) -> Dict[str, Any]: + return metrics_for_notes( + notes, + params["dominance_threshold"], + params["even_high"], + params["even_low"], + params["hybrid_alpha"], + params["homogeneity_method"], + bin_cents=params["bin_cents"], + chain_threshold=params["chain_threshold"], + intervallic_headline_mode=IntervallicHeadlineMode.PAIRWISE, + ) + + +def _notes_for_fixture(fixture_id: str) -> List[NoteTuple]: + payload = _load_fixture(fixture_id) + if "notes" in payload: + return notes_from_json_rows(payload["notes"]) + if "slices" in payload: + return notes_from_json_rows(payload["slices"][0]["notes"]) + raise KeyError(fixture_id) + + +def _analyze_fixture(fixture_id: str) -> Dict[str, Any]: + payload = _load_fixture(fixture_id) + params = _analysis_params(payload) + if fixture_id == PASSAGE_FIXTURE: + rows: List[Dict[str, Any]] = [] + for sl in payload["slices"]: + notes = notes_from_json_rows(sl["notes"]) + m = _metrics(notes, params) + rows.append( + { + "slice_id": sl["slice_id"], + "label": sl["label"], + "metrics": m, + "analyzed": analyze_sonority( + notes, + hybrid_alpha=params["hybrid_alpha"], + homogeneity_method=params["homogeneity_method"], + bin_cents=params["bin_cents"], + ), + } + ) + return {"kind": "passage", "rows": rows, "params": params} + notes = notes_from_json_rows(payload["notes"]) + return { + "kind": "aggregate", + "notes": notes, + "params": params, + "metrics": _metrics(notes, params), + "analyzed": analyze_sonority( + notes, + hybrid_alpha=params["hybrid_alpha"], + homogeneity_method=params["homogeneity_method"], + bin_cents=params["bin_cents"], + ), + } + + +@pytest.mark.parametrize("fixture_id", ALL_FIXTURE_IDS) +def test_all_interval_regression_fixtures_parse_or_analyze(fixture_id: str): + result = _analyze_fixture(fixture_id) + if result["kind"] == "passage": + assert len(result["rows"]) >= 2 + for row in result["rows"]: + assert row["metrics"]["H"] is not None + else: + assert result["metrics"]["H"] is not None + assert result["analyzed"]["pair_score"] is not None + + +def test_transposition_invariance(): + base_id = "chromatic_cluster_four" + trans_id = "transposed_same_structure" + base = _analyze_fixture(base_id) + trans = _analyze_fixture(trans_id) + tol = 1e-9 + for key in ("pair_score", "chain_score", "H"): + assert base["metrics"][key] == pytest.approx(trans["metrics"][key], abs=tol) + analyzed_keys = ("pair_score", "chain_score", "hybrid_score") + for key in analyzed_keys: + assert base["analyzed"][key] == pytest.approx(trans["analyzed"][key], abs=tol) + + +def test_dyad_concentration_not_consonance(): + """ + Perfect fifth and minor second dyads both show single-interval concentration. + + This does NOT validate acoustic or perceptual consonance — only interval-type + concentration under the symbolic distance model. + """ + fifth = _analyze_fixture("single_interval_dyad_perfect_fifth") + minor = _analyze_fixture("minor_second_dyad") + for label, result in (("fifth", fifth), ("minor_second", minor)): + m = result["metrics"] + assert m["pair_score"] == pytest.approx(1.0), label + assert m["chain_score"] == pytest.approx(1.0), label + assert m["H"] == pytest.approx(1.0), label + assert len(m["distance_counts"]) <= 1, label + + +@pytest.mark.parametrize( + "fixture_id", + ["chromatic_cluster_four", "whole_tone_segment_four"], +) +def test_adjacent_vs_pairwise_distinction(fixture_id: str): + result = _analyze_fixture(fixture_id) + m = result["metrics"] + assert m["chain_score"] > m["pair_score"] + assert m["chain_score"] >= 0.95 + assert m["pair_score"] <= 0.55 + + +def test_same_cardinality_different_distribution(): + a = _analyze_fixture("same_cardinality_different_distribution_A") + b = _analyze_fixture("same_cardinality_different_distribution_B") + assert len(a["notes"]) == len(b["notes"]) == 4 + assert a["metrics"]["distance_counts"] != b["metrics"]["distance_counts"] + assert a["metrics"]["pair_score"] != pytest.approx(b["metrics"]["pair_score"]) + + +def test_symmetric_chord_vs_irregular_chord(): + sym = _analyze_fixture("diminished_seventh_symmetric") + irr = _analyze_fixture("dominant_seventh_irregular") + assert len(sym["notes"]) == len(irr["notes"]) == 4 + assert sym["metrics"]["pair_score"] > irr["metrics"]["pair_score"] + assert sym["metrics"]["chain_score"] > irr["metrics"]["chain_score"] + + aug = _analyze_fixture("augmented_triad_symmetric") + maj = _analyze_fixture("major_triad_close") + assert aug["metrics"]["pair_score"] > maj["metrics"]["pair_score"] + + +def test_repeated_pitch_policy(): + payload = _load_fixture("repeated_pitch_density_not_homogeneity") + params = _analysis_params(payload) + raw_notes = notes_from_json_rows(payload["notes"]) + deduped = dedupe_notes_by_midi(raw_notes, bin_cents=params["bin_cents"]) + + raw_metrics = _metrics(raw_notes, params) + dedup_metrics = _metrics(deduped, params) + + assert len(raw_notes) == 4 + assert len(deduped) == 2 + assert raw_metrics["pair_score"] != pytest.approx(dedup_metrics["pair_score"]) + assert dedup_metrics["pair_score"] == pytest.approx(1.0) + assert raw_metrics["pair_score"] == pytest.approx(0.5) + + +def test_inversion_preserves_interval_multiset(): + base = _analyze_fixture("major_triad_close") + inv = _analyze_fixture("inversion_same_interval_multiset") + assert base["metrics"]["pair_score"] == pytest.approx(inv["metrics"]["pair_score"]) + assert sorted(base["metrics"]["distance_counts"].values()) == sorted( + inv["metrics"]["distance_counts"].values() + ) + assert sorted(base["metrics"]["distance_counts"].keys()) == sorted( + inv["metrics"]["distance_counts"].keys() + ) + + +def test_edo_or_binning_sensitivity(): + micro = _analyze_fixture("EDO_24_microtonal_regular") + assert micro["metrics"]["H"] is not None + assert micro["metrics"]["distance_counts"] + + payload = _load_fixture("EDO_bin_change_sensitivity") + notes = notes_from_json_rows(payload["notes"]) + params = _analysis_params(payload) + m100 = _metrics(notes, params) + m50 = _metrics(notes, {**params, "bin_cents": 50}) + assert m100["H"] != pytest.approx(m50["H"]) or m100["distance_counts"] != m50["distance_counts"] + + +def test_passage_interval_field_change(): + result = _analyze_fixture(PASSAGE_FIXTURE) + rows = result["rows"] + assert len(rows) == 3 + + profiles = [row["metrics"]["distance_counts"] for row in rows] + evenness = [row["metrics"]["evenness_score"] for row in rows] + assert len({json.dumps(p, sort_keys=True) for p in profiles}) > 1 + assert len({round(e, 4) for e in evenness}) > 1 + + summary_rows = [ + {"Slice": row["slice_id"], "H (interval dominance)": row["metrics"]["H"]} + for row in rows + ] + with_delta = passage_delta_rows(summary_rows) + assert len(with_delta) == 3 + assert "ΔH (prev slice)" in with_delta[0] + assert any(r["ΔH (prev slice)"] is not None for r in with_delta[1:]) + + +def test_unison_dyad_degenerate_case(): + payload = _load_fixture("unison_dyad") + notes = notes_from_json_rows(payload["notes"]) + card = vertical_cardinality_for_notes(notes, bin_cents=100, edo=12) + assert card["vertical_note_count"] == 2 + assert card["vertical_unique_pitch_count"] == 1 + m = _metrics(notes, _analysis_params(payload)) + assert m["pair_score"] == pytest.approx(1.0) + + +def test_octave_duplication_uses_distinct_heights(): + notes = _notes_for_fixture("octave_duplication_case") + m = _metrics(notes, _analysis_params(_load_fixture("octave_duplication_case"))) + assert ("C", 0.0, 4) in notes and ("C", 0.0, 5) in notes + assert len(m["distance_counts"]) > 1 + + +def test_documentation_lists_all_fixtures(): + text = DOCS_PATH.read_text(encoding="utf-8") + manifest = json.loads((FIXTURE_DIR / "manifest.json").read_text(encoding="utf-8")) + for fixture_id in manifest["fixtures"]: + assert fixture_id in text, f"{fixture_id} missing from documentation" + assert "not perceptual" in text.lower() or "not acoustic" in text.lower() + assert "pairwise" in text.lower() + assert "adjacent" in text.lower() or "chain" in text.lower()