diff --git a/crates/epiphany-engrave/DECISIONS.md b/crates/epiphany-engrave/DECISIONS.md index d163a05..5e805eb 100644 --- a/crates/epiphany-engrave/DECISIONS.md +++ b/crates/epiphany-engrave/DECISIONS.md @@ -259,7 +259,7 @@ resolved: ## Quality metrics (2026-07) — decisions -The Quality Metric Catalog companion (v0.1.0) ratified the nine normative +The Quality Metric Catalog companion (v0.2.0) ratified the nine normative axes' formal definitions, anchors, thresholds, and the `QualityFloorApproached` trigger; `Engraver::resolve` now computes the real vector (the private `quality` module), replacing the all-worst placeholder. @@ -336,11 +336,34 @@ testkit's reference-suite harness. documented xfail row in the testkit harness, P12-I11); the **v3** widow-rebalance phase (casting-off decision 9) evens the split to `casting_off` = 0.4463, clearing the miss with no catalog change, and the - xfail row is promoted to a plain Pass. One finding the catalog's - threshold-tuning open question anticipated remains (a Pass-12/QMC candidate - below, P12-I12): `spacing_distortion` on 3–8-column entries (0.36–0.41) sits - above the Standard column's 0.32 warning floor, so short scores warn under - the default Standard profile — a *diagnostic* floor, not a Minimal failure. + xfail row is promoted to a plain Pass. The second finding (P12-I12): three + short entries measured `spacing_distortion` 0.36–0.41, above the Standard + 0.32 warning floor — a spurious *diagnostic* (never a Minimal failure). It + was resolved by a catalog refinement (QMC 0.1.0 → 0.2.0), see quality + decision 8: `spacing_distortion` is scoped to rhythmic (note/rest) columns, + dropping the three to 0.2188 / 0.0819 / 0.0856 — below the floor, no code + layout change. +8. **Rhythmic-column spacing (`spacing_distortion` scoped) — the honest P12-I12 + fix.** The measured false positive was that a short healthy line's wide + clef-to-first-note lead advance (furniture width, not note spacing) inflated + the per-system advance CV above the Standard warning floor. The catalog + (QMC 0.2.0) scopes the axis to **rhythmic columns** — spring slots bearing a + notehead or rest — excluding the clef/key/time lead and treating barlines + transparently (a note-to-note advance spans them). `quality::census` now + builds `columns` only from slots in the precomputed rhythmic set + (`is_rhythmic`: a `notehead*`/`rest*` glyph anywhere in the slot); the CV and + contributing-unit rule (≥ 3 rhythmic columns) are otherwise unchanged. This + is the mirror of the I11 resolution — measure the right thing rather than + relax the threshold — but here the defect lived in the normative metric + definition, so it *is* a catalog change (unlike I11). Measurement-only: the + resolved layout, canonical bytes, render goldens, and `ENGRAVER_VERSION` are + untouched; only the reported `spacing_distortion` value changes (RS-3/5/6 + drop below the floor, RS-2/RS-4 go vacuous-0.0 as their systems carry < 3 + rhythmic columns — honestly "too little to measure"). The floor-column + regression test was re-pointed from b-flat's spacing (which no longer warns) + to RS-1's casting-off (which still sits between the Standard and Minimal + floors); a new `short_scores_do_not_trip_the_standard_spacing_floor` locks + the fix. The duration-aware optical-spacing open question stays open. ### Pass 12 candidates (quality metrics) @@ -354,11 +377,14 @@ testkit's reference-suite harness. an RS-1 override) was deliberately **not** taken: the `0.5` anchor and the `0.90` Minimal column stood. `ENGRAVER_VERSION` 2 → 3; `ten_measure` render goldens regenerated. -- **P12 (proposed) — QMC: the Standard spacing floor warns on short scores.** - With uniform preferred widths, few-column systems (3–8 columns with a wide - clef/key lead) measure spacing CV 0.36–0.41 — above the Standard column's - 0.8 × 0.40 = 0.32 warning floor, so the default profile emits - `QualityFloorApproached(Spacing)` on tiny, healthy scores. Consider either - a duration/lead-aware refinement of the axis (the catalog's optical-spacing - open question) or excluding the lead column from the advance sequence in a - QMC minor revision. +- **P12-I12 — RESOLVED (QMC 0.2.0, rhythmic-column spacing).** With uniform + preferred widths, few-column systems (3–8 columns with a wide clef/key lead) + measured spacing CV 0.36–0.41 — above the Standard column's 0.8 × 0.40 = 0.32 + warning floor, so the default profile emitted `QualityFloorApproached(Spacing)` + on tiny, healthy scores. Resolved the **lead-aware way**: the catalog scopes + `spacing_distortion` to rhythmic (note/rest) columns, excluding the + clef/key/time furniture lead and treating barlines transparently (quality + decision 8). The three entries drop to 0.2188 / 0.0819 / 0.0856 (below the + floor). The alternative — a duration-proportional (optical) redefinition — + needs the pipeline's deferred duration-aware preferred widths and stays the + catalog's open question. diff --git a/crates/epiphany-engrave/src/quality.rs b/crates/epiphany-engrave/src/quality.rs index c338a2e..184ed8f 100644 --- a/crates/epiphany-engrave/src/quality.rs +++ b/crates/epiphany-engrave/src/quality.rs @@ -1,5 +1,5 @@ //! **Real quality-metric computation** — the nine normative axes of the -//! *Quality Metric Catalog* companion (v0.1.0, Chapter 3), measured over what +//! *Quality Metric Catalog* companion (v0.2.0, Chapter 3), measured over what //! the pipeline already produced: the resolved world-frame geometry //! ([`CastLayout`]), the constrained input (slot identity, vertical bands), and //! the declared page geometry. Normalization anchors, threshold tables, and the @@ -20,9 +20,12 @@ //! excluding same-slot pairs (slot identity = the glyph's //! `horizontal_slot` in the constrained input; the resolved glyph list is //! index-parallel to it) and strokes (not glyphs, never swept). -//! * **`spacing_distortion`** — per-system column advances: the distinct -//! resolved x of each glyph-bearing slot realized in the system (its first -//! member's baseline — the spacing pass's own column reference). +//! * **`spacing_distortion`** — per-system advances between **rhythmic** +//! columns: the distinct resolved x of each note/rest-bearing slot realized +//! in the system (its first member's baseline — the spacing pass's own column +//! reference). The clef/key/time lead and barlines bear no notehead or rest, +//! so they contribute no column and a note-to-note advance spans them +//! (catalog §`spacing_distortion` — measuring rhythmic spacing, not furniture). //! * **`slur_shape_penalty` / `beam_slope_penalty`** — **vacuous 0.0**: the //! pipeline draws no slur or beam geometry (slurs/beams exist logically, //! not as curves/segments), so the contributing-unit sets are empty. The @@ -108,14 +111,33 @@ struct SystemCensus { members: Vec>, /// Glyph-ink span `w_s` per system (0 for a glyph-less system). width: Vec, - /// Column reference x per realized slot per system, ascending and distinct. + /// Reference x of each **rhythmic** column (a slot bearing a notehead or + /// rest) per system, ascending and distinct — the spacing axis's domain + /// (catalog §`spacing_distortion`: the clef/key/time lead and barlines are + /// furniture, not rhythmic columns). columns: Vec>, } +/// Whether a glyph is a notehead or a rest — the mark that makes its slot a +/// **rhythmic column** (catalog §`spacing_distortion`). +fn is_rhythmic(name: &str) -> bool { + name.starts_with("notehead") || name.starts_with("rest") +} + fn census(input: &ConstrainedLayoutIR, cast: &CastLayout) -> SystemCensus { let count = cast.region_of_system.len(); let mut members: Vec> = vec![Vec::new(); count]; let mut spans: Vec> = vec![None; count]; + // Rhythmic slots: those bearing a notehead or rest. Precomputed because a + // slot's rhythmic status depends on *all* its members (a note's accidental + // may precede its notehead in input order), and only rhythmic slots become + // spacing columns (catalog §`spacing_distortion`, "rhythmic column"). + let rhythmic: BTreeSet = input + .glyphs + .iter() + .filter(|glyph| is_rhythmic(glyph.glyph.as_str())) + .map(|glyph| glyph.horizontal_slot) + .collect(); // Column reference: the slot's first member (input order) — the same // convention the spacing and casting passes use for a slot's reference x. let mut columns: Vec> = vec![BTreeMap::new(); count]; @@ -131,9 +153,13 @@ fn census(input: &ConstrainedLayoutIR, cast: &CastLayout) -> SystemCensus { Some((lo, hi)) => (lo.min(left), hi.max(right)), None => (left, right), }); - columns[system] - .entry(glyph.horizontal_slot) - .or_insert_with(|| f64::from(cast.glyphs[index].position.x.0)); + // Only rhythmic columns carry spacing advances; a note-to-note advance + // spans any intervening barline or furniture (which contribute no column). + if rhythmic.contains(&glyph.horizontal_slot) { + columns[system] + .entry(glyph.horizontal_slot) + .or_insert_with(|| f64::from(cast.glyphs[index].position.x.0)); + } } let width = spans .iter() @@ -196,7 +222,8 @@ fn collision_raw(input: &ConstrainedLayoutIR, cast: &CastLayout, census: &System } /// `spacing_distortion` (catalog §`spacing_distortion`): mean per-system CV of -/// column advances, over systems realizing at least three columns. +/// **rhythmic** column advances, over systems realizing at least three rhythmic +/// columns (note/rest-bearing slots — see [`census`]). fn spacing_raw(census: &SystemCensus) -> f64 { let mut per_system = Vec::new(); for columns in &census.columns { @@ -586,18 +613,17 @@ mod tests { #[test] fn floor_warnings_reference_the_profiles_threshold_column() { - // The b-flat scale's spacing distortion (~0.41: eight columns whose - // flat-bearing columns advance wider) sits between the Standard - // column's floor (0.8 x 0.40 = 0.32) and the Minimal column's - // (0.8 x 0.90 = 0.72) — so the default Standard profile warns about - // Spacing and the Draft profile (which selects the Minimal column per - // the catalog's profile registry) does not. - let score = epiphany_testkit::corpus::corpus() - .into_iter() - .find(|fixture| fixture.name == "b_flat_major_scale") - .expect("corpus entry exists"); - let input = to_constrained(&to_logical(&(score.build)())); - let spacing_warned = |profile: SolverProfile| { + // The ten-measure fixture's casting-off distortion (~0.45: the + // six/four widow-rebalanced split) sits between the Standard column's + // floor (0.8 x 0.35 = 0.28) and the Minimal column's (0.8 x 0.90 = + // 0.72) — so the default Standard profile warns about CastingOff and + // the Draft profile (which selects the Minimal column per the catalog's + // profile registry) does not. (This is the profile-column contrast the + // b-flat scale's spacing used to show, before spacing_distortion was + // scoped to rhythmic columns and short scores stopped warning; the + // casting-off axis carries the demonstration now.) + let input = ten_measure(); + let castoff_warned = |profile: SolverProfile| { let config = SolverConfig { profile, ..SolverConfig::default() @@ -610,22 +636,55 @@ mod tests { matches!( w.kind, SolverWarningKind::QualityFloorApproached { - metric: QualityMetricKind::Spacing + metric: QualityMetricKind::CastingOff } ) }) }; - assert!(spacing_warned(SolverProfile::Standard)); - assert!(spacing_warned(SolverProfile::Publication)); - assert!(!spacing_warned(SolverProfile::Draft)); + assert!(castoff_warned(SolverProfile::Standard)); + assert!(castoff_warned(SolverProfile::Publication)); + assert!(!castoff_warned(SolverProfile::Draft)); // The metric itself is profile-independent — only the diagnostic // column changes. let value = Engraver::default() .solve(&input, &SolverConfig::default()) .metric_vector - .spacing_distortion + .casting_off_quality .0; - assert!((0.32..=0.72).contains(&value), "spacing = {value}"); + assert!((0.28..=0.72).contains(&value), "casting_off = {value}"); + } + + #[test] + fn short_scores_do_not_trip_the_standard_spacing_floor() { + // P12-I12 regression. Before `spacing_distortion` was scoped to rhythmic + // columns, a short healthy line's wide clef-to-first-note lead advance + // inflated the CV above the Standard warning floor (0.8 x 0.40 = 0.32), + // so these three-to-eight-column corpus entries spuriously warned + // (measured 0.36-0.41). Scoped to note/rest columns — the clef/key/time + // lead bears no notehead, so it contributes no column — none does. + let floor = 0.8 * 0.40; + for name in ["b_flat_major_scale", "notes_and_rests", "meter_three_four"] { + let score = epiphany_testkit::corpus::corpus() + .into_iter() + .find(|fixture| fixture.name == name) + .expect("corpus entry exists"); + let input = to_constrained(&to_logical(&(score.build)())); + let report = Engraver::default().solve(&input, &SolverConfig::default()); + assert!( + report.metric_vector.spacing_distortion.0 < floor, + "{name}: spacing {} still above the Standard floor {floor}", + report.metric_vector.spacing_distortion.0 + ); + assert!( + !report.warnings.iter().any(|w| matches!( + w.kind, + SolverWarningKind::QualityFloorApproached { + metric: QualityMetricKind::Spacing + } + )), + "{name}: spurious spacing floor warning under the default Standard profile" + ); + } } #[test] diff --git a/crates/epiphany-layout-ir/DECISIONS.md b/crates/epiphany-layout-ir/DECISIONS.md index eafa42c..cc925be 100644 --- a/crates/epiphany-layout-ir/DECISIONS.md +++ b/crates/epiphany-layout-ir/DECISIONS.md @@ -425,7 +425,7 @@ object is covered); the provenance-preservation contract itself is unchanged. ## Quality Metric Catalog constants (`src/quality.rs`, 2026-07) **Decision: the catalog's normative constants live in this crate, as a pure -transcription.** The Quality Metric Catalog companion (v0.1.0) pins the nine +transcription.** The Quality Metric Catalog companion (v0.2.0) pins the nine axes' normalization anchors (`R_worst`), the clamped-linear normalization form `n = min(1, raw / R_worst)`, the Minimal/Standard threshold table, the `QualityFloorApproached` warning fraction (0.8), and the tier/profile → diff --git a/crates/epiphany-layout-ir/src/quality.rs b/crates/epiphany-layout-ir/src/quality.rs index 2d5c5c3..e32b9fe 100644 --- a/crates/epiphany-layout-ir/src/quality.rs +++ b/crates/epiphany-layout-ir/src/quality.rs @@ -1,7 +1,10 @@ //! The Quality Metric Catalog's normative constants (companion specification -//! *Epiphany — Quality Metric Catalog*, v0.1.0): the per-axis normalization +//! *Epiphany — Quality Metric Catalog*, v0.2.0): the per-axis normalization //! anchors, the per-tier metric threshold table, the profile→threshold-column -//! mapping, and the `QualityFloorApproached` warning fraction. +//! mapping, and the `QualityFloorApproached` warning fraction. (The v0.2.0 +//! `spacing_distortion` refinement scoped that axis's *measurement domain* to +//! rhythmic columns; it touched no anchor or threshold, so this transcription +//! of the numeric constants is unchanged.) //! //! This module is a **transcription**, not an invention: every number here is //! pinned by the catalog and cited to its chapter. Solvers that compute real @@ -202,7 +205,7 @@ pub fn tier_thresholds(tier: SolverTier) -> Option<&'static MetricThresholds> { /// The threshold column a **registered profile** selects (catalog Chapter 6, /// `req:qmc:profiles`): `Draft` → the Minimal column (few warnings, fast /// iteration); `Standard` and `Publication` → the Standard column (no column -/// tighter than Standard is ratified in v0.1). This is the column the solver's +/// tighter than Standard is ratified as of v0.2). This is the column the solver's /// own `QualityFloorApproached` diagnostics reference during ordinary solves; /// suite evaluation at a claimed tier always uses that *tier's* column /// ([`tier_thresholds`]). diff --git a/crates/epiphany-testkit/src/reference_suite.rs b/crates/epiphany-testkit/src/reference_suite.rs index dc93052..93fb6e4 100644 --- a/crates/epiphany-testkit/src/reference_suite.rs +++ b/crates/epiphany-testkit/src/reference_suite.rs @@ -122,7 +122,7 @@ pub fn entries() -> Vec { construction: "fixtures::ten_measure_single_staff(0x000A_11CE)", build: rs1, // Passes every Minimal threshold under the reference engraver - // (engrave v3, QMC v0.1.0 anchors). Greedy first-fit alone left a + // (engrave v3, QMC v0.2.0 anchors). Greedy first-fit alone left a // two-measure stub last system (width CV 0.61 -> clamped 1.0 > the // 0.90 threshold, the original P12-I11 miss); casting-off's // widow-rebalance phase now evens the split to a six/four diff --git a/spec/PASS12_BATCH.md b/spec/PASS12_BATCH.md index 0209685..436c924 100644 --- a/spec/PASS12_BATCH.md +++ b/spec/PASS12_BATCH.md @@ -67,7 +67,7 @@ code instead is the failure mode this batch exists to prevent. | P12-I9 | `epiphany-layout-ir` I | Honouring a user break must attribute the decision to its override (`DecisionSource::UserOverride(id)`), but constraints carry no override identity; implemented via a `ConstrainedLayoutIR.break_origins` sidecar populated by `to_constrained`. Bless the sidecar or widen the normalized constraint record. | G / Pass 12 (solver) | | P12-I10 | `epiphany-layout-ir` I | System-spanning strokes split at system boundaries need synthesized provenance for continuation segments; implemented as `SynthesisKind::Registered(SYSTEM_CONTINUATION_SYNTHESIS)` with a deterministic `(original, ordinal)` instance key. Add a first-class continuation synthesis kind or bless the registered id. | G / Pass 12 (provenance) | | ~~P12-I11~~ **RESOLVED (engrave v3 widow-rebalance)** | `epiphany-engrave` I | RS-1 honestly failed the Minimal casting-off threshold under the reference engraver (measured 1.0 vs 0.90): greedy first-fit left a two-measure stub last system (width CV 0.6145 ≥ the 0.5 anchor). **Resolved the honest way — an engrave-side casting-off balance pass, no catalog change:** casting-off gained a second **widow-rebalance** phase that moves whole trailing measures from a region's penultimate system into its final one, choosing the shift that minimizes the larger of the two distribution penalties the catalog defines for the break family (width imbalance vs non-final underfill; both share the 0.5 anchor). RS-1 now casts six/four instead of eight/two: casting_off 1.0 → **0.4463**, system_break 0.254 → 0.677, every axis ≤ 0.90. `ENGRAVER_VERSION` 2 → 3; render goldens regenerated; the suite's Xfail row promoted to a plain Pass. The `casting_off` 0.5 anchor and the Minimal column stood — the engraver improved, not the threshold — so the Quality Metric Catalog is normatively unchanged. | ✅ done | -| P12-I12 | `epiphany-engrave` I | The Standard-tier spacing floor warns on short healthy scores: 3–8-column entries with a wide clef/key lead measure spacing CV 0.36–0.41 > the 0.32 Standard floor. Consider a lead-aware or duration-aware refinement of the spacing_distortion raw measurement (the catalog's optical-spacing open question). | G / Pass 12 (quality) | +| ~~P12-I12~~ **RESOLVED (QMC 0.2.0: rhythmic-column spacing)** | `epiphany-engrave` I | The Standard-tier spacing floor warned on short healthy scores: 3–8-column entries with a wide clef/key lead measured spacing CV 0.36–0.41 > the 0.32 Standard floor (a spurious *diagnostic* — the scores all passed Minimal). **Resolved by the lead-aware refinement (Quality Metric Catalog 0.1.0 → 0.2.0):** `spacing_distortion` is now scoped to the system's **rhythmic columns** (slots bearing a notehead or rest), excluding the clef / key-signature / time-signature lead and treating barlines transparently (a note-to-note advance spans them) — so the furniture-width lead gap no longer inflates the CV. The three entries drop to 0.2188 / 0.0819 / 0.0856 (all below the 0.32 floor), and the axis stays honest on real irregularity (RS-3 keeps 0.2188 from a mid-line accidental). Anchor, orientation, range, thresholds, and the eight other axes unchanged; no layout/golden change (measurement-only, `ENGRAVER_VERSION` untouched). The **duration-aware optical-spacing** open question (deviation from duration-proportional spacing) stays open — it needs the pipeline's deferred duration-proportional preferred widths. | ✅ done | ## Not yet open elsewhere @@ -80,10 +80,12 @@ work added C1..C4 (re-anchoring), D1 (bundle operation index), and E1..E5 value-restoring undo, 2026-07-02) added C5, K8..K11, and I7..I10. The second tranche (Quality Metric Catalog 0.1.0 + Reference Suite 0.1.0 + real engraver metrics + the suite harness + the multi-system click fix, -2026-07-03) added I11..I12. A follow-up (2026-07-03) resolved I11 (struck -through above): casting-off gained a widow-rebalance phase (engrave v3), so -RS-1 passes every Minimal threshold with no catalog change — I12 (the -Standard-tier spacing floor on short scores) stays open. +2026-07-03) added I11..I12. Follow-ups (2026-07-03) resolved both (struck +through above): I11 by an engrave-side casting-off widow-rebalance phase +(engrave v3, no catalog change), and I12 by scoping `spacing_distortion` to +rhythmic columns (Quality Metric Catalog 0.1.0 → 0.2.0). The only remaining +spacing follow-up is the catalog's duration-aware optical-spacing open +question, which lives in the companion, not as a batch row. Agent J's Binary Format companion now exists (`spec/binary_format.tex`, v0.1.0): it ratified the P12-D1/E1/E2/E3 inputs (struck through above) and discharged the crates' provisional-codec notes diff --git a/spec/PASS12_RATIFICATION_LOG.md b/spec/PASS12_RATIFICATION_LOG.md index 87d09a2..b760acc 100644 --- a/spec/PASS12_RATIFICATION_LOG.md +++ b/spec/PASS12_RATIFICATION_LOG.md @@ -60,3 +60,17 @@ record that the Quality Metric Catalog and the core spec were **not** changed. | Item | Disposition | Spec locus | Consumer | |---|---|---|---| | P12-I11 casting-off stub last system | **no spec change (engrave fix)** — casting-off gained a second **widow-rebalance** phase (`epiphany-engrave` v3): it moves whole trailing measures from a region's penultimate system into its final one, choosing the shift that minimizes the larger of the two distribution penalties the catalog already defines for the break family (`casting_off_quality` width imbalance vs `system_break_penalty` non-final underfill; both share the 0.5 anchor). RS-1 casts six/four instead of eight/two — `casting_off` 1.0 → 0.4463, every axis ≤ 0.90 — so the suite's asserted Xfail row is promoted to a plain Pass. This is the deliberately-chosen *honest* resolution: the `casting_off` 0.5 anchor and the Minimal 0.90 column were **vindicated, not relaxed** (the engraver improved, no anchor rescale / threshold loosening / RS-1 per-entry override). Core spec Chapter 9's "Minimal makes no optimality claim" already permits the heuristic; nothing normative changed | — (no spec locus; core spec + Quality Metric Catalog unchanged) | `epiphany-engrave` (`casting::rebalance_widows`, `ENGRAVER_VERSION` 3); `epiphany-testkit` (RS-1 xfail row removed); render goldens regenerated | + +## P12-I12 resolution (2026-07-03) — Quality Metric Catalog 0.1.0 → 0.2.0 + +Unlike P12-I11, the second measured finding's honest fix is a **catalog +change**: the defect was in the metric's own definition, not the engraver. + +| Item | Disposition | Spec locus | Consumer | +|---|---|---|---| +| P12-I12 spacing floor on short scores | **fixed (metric measurement domain)** — `spacing_distortion`'s raw measurement is scoped to the system's **rhythmic columns** (spring slots bearing a notehead or rest); the clef / key-signature / time-signature lead and barlines contribute no column, so a note-to-note advance spans them. Rationale: the leading clef-to-first-note gap is furniture width, not note spacing, and folding it into the CV flagged perfectly-spaced short lines as distorted purely for carrying a clef. The three warning entries drop from 0.36–0.41 to 0.2188 / 0.0819 / 0.0856 (below the Standard 0.32 floor), and the axis stays live on real irregularity. The `1.0` anchor, orientation, range, tier thresholds, and the eight other axes are **unchanged**; this is measurement-only — no layout, canonical bytes, render golden, or `ENGRAVER_VERSION` change. Catalog **0.1.0 → 0.2.0** (a normative refinement of one axis's measurement domain). The duration-aware optical-spacing open question stays open | quality\_metric\_catalog §`spacing_distortion` (`req:qmc:spacing`); revision-history row 0.2.0 | `epiphany-engrave` (`quality::census` rhythmic-column filter, `is_rhythmic`); `epiphany-testkit` (RS-3/5/6 measured values drop, still Pass) | + +**Version movements.** Quality Metric Catalog 0.1.0 → 0.2.0 +(`spacing_distortion` scoped to rhythmic columns). Core spec unchanged (the +axis's formal definition is delegated to the catalog; Chapter 9 carries only +the field, which is unchanged). diff --git a/spec/quality_metric_catalog.pdf b/spec/quality_metric_catalog.pdf index 2eb9ade..a709806 100644 Binary files a/spec/quality_metric_catalog.pdf and b/spec/quality_metric_catalog.pdf differ diff --git a/spec/quality_metric_catalog.tex b/spec/quality_metric_catalog.tex index 87904bf..6422fd0 100644 --- a/spec/quality_metric_catalog.tex +++ b/spec/quality_metric_catalog.tex @@ -226,7 +226,7 @@ {\Large\scshape\color{epiphanyslate}Quality Metric Catalog}\\[6pt] {\large\itshape\color{epiphanyslate}A companion to the Core Specification}\\[14pt] {\color{epiphanygold}\rule{3in}{0.8pt}}\\[24pt] - {\normalsize\color{epiphanyink}Version 0.1.0 --- Phase 3 (the normative metric set: formal definitions, normalization, weights, tier thresholds, profile registry)}\\[4pt] + {\normalsize\color{epiphanyink}Version 0.2.0 --- Phase 3 (the normative metric set: formal definitions, normalization, weights, tier thresholds, profile registry; \texttt{spacing\_distortion} scoped to rhythmic columns)}\\[4pt] {\small\color{epiphanyslate}Normative for the metrics and thresholds it defines} \vfill \end{titlepage} @@ -246,7 +246,7 @@ deliver ``per-metric normalization functions mapping raw measurements to metric thresholds, and the formal definition of each quality metric in the normative metric set.'' -This release (v0.1.0) delivers all four chartered items, plus two small +This companion (v0.2.0) delivers all four chartered items, plus two small registries the core specification names but defers here: \begin{itemize} @@ -721,44 +721,62 @@ the resolved layout already carries. \section{\texttt{spacing\_distortion}} \label{sec:metrics:spacing} -\textbf{Phenomenon.} Uneven horizontal distribution within a system: -columns bunched together here and stretched apart there, where the -underlying spring model asked for near-uniform advances. +\textbf{Phenomenon.} Uneven horizontal distribution within a system: the +\emph{rhythmic} columns --- those carrying notes and rests --- bunched +together here and stretched apart there, where the underlying spring model +asked for near-uniform advances. \begin{requirement} \label{req:qmc:spacing} -\textbf{Contributing units:} systems $s$ with $m_s \ge 3$ (at least two -advances). +\textbf{Rhythmic columns.} A \emph{rhythmic column} of a system is a +horizontal spring slot at least one of whose glyphs is a notehead or a +rest. The clef, key-signature, and time-signature lead and the barlines are +\textbf{not} rhythmic columns: their horizontal extent is notational +furniture, fixed by their content rather than by rhythm. -\textbf{Raw measurement:} per unit, -$\mathit{raw}_s = \mathrm{CV}(a^s_1, \dots, a^s_{m_s - 1})$; the axis raw -value is the arithmetic mean of $\mathit{raw}_s$ over contributing units. +\textbf{Contributing units:} systems $s$ with at least three rhythmic +columns (so at least two rhythmic advances). + +\textbf{Raw measurement:} let $x^s_1 < \dots < x^s_{k_s}$ be the reference +x-positions of the system's $k_s$ rhythmic columns in order, and +$a^s_i = x^s_{i+1} - x^s_i$ the advance between consecutive rhythmic columns +(a note-to-note advance spans any barline or furniture that falls between +the two, so barlines are transparent, not breaks in the sequence). Then +$\mathit{raw}_s = \mathrm{CV}(a^s_1, \dots, a^s_{k_s - 1})$, and the axis +raw value is the arithmetic mean of $\mathit{raw}_s$ over contributing +units. \textbf{Normalization:} $R_{\mathrm{worst}} = 1.0$; $n = \min(1, \mathit{raw})$. \end{requirement} \begin{rationale} -A coefficient of variation of $1.0$ means the typical column advance -deviates from the mean by the whole mean --- spacing with no discernible -regularity. v0.1 defines \emph{geometric} regularity deliberately: the -reference spring model's preferred widths are uniform, so regular advances -are exactly what its ideal output looks like, and the collision minima -(accidental overhangs, wide columns) that legitimately perturb advances -are modest on realistic scores. +A coefficient of variation of $1.0$ means the typical advance deviates from +the mean by the whole mean --- spacing with no discernible regularity. v0.2 +defines \emph{geometric} regularity over the \emph{rhythmic} columns +deliberately: the reference spring model's preferred widths are uniform, so +regular note/rest advances are exactly what its ideal output looks like, +and the collision minima (accidental overhangs, wide columns) that +legitimately perturb those advances are modest on realistic scores. The +axis is scoped to rhythmic columns because a leading clef, key signature, +or time signature is furniture whose width the spring model does not govern: +folding the wide clef-to-first-note gap into the CV would flag a +perfectly-spaced short line as distorted purely for carrying a clef --- +measuring furniture, not spacing. \end{rationale} \begin{openquestion} \textbf{Optical spacing at the Standard tier.} Mature engraving spaces -columns proportionally to musical duration (with an optical correction), -not uniformly; under a duration-proportional model, this axis's ideal -would be ``advances proportional to the column's duration share,'' and a -perfectly optically-spaced line would score \emph{worse} than a uniform -one under the v0.1 definition. When the layout pipeline gains -duration-aware preferred widths, should the Standard tier redefine +rhythmic columns proportionally to musical duration (with an optical +correction), not uniformly; under a duration-proportional model, this +axis's ideal would be ``advances proportional to the column's duration +share,'' and a perfectly optically-spaced line would score \emph{worse} +than a uniform one under the v0.2 definition. When the layout pipeline +gains duration-aware preferred widths, should the Standard tier redefine $\mathit{raw}_s$ as deviation from the duration-proportional ideal while -Minimal keeps geometric regularity? v0.1 defines geometric regularity -only. +Minimal keeps geometric regularity? v0.2 scopes the geometric definition to +rhythmic columns (removing the leading-furniture false positive on short +scores) but still measures geometric, not duration-proportional, regularity. \end{openquestion} \section{\texttt{slur\_shape\_penalty}} @@ -1249,7 +1267,7 @@ selections: fraction is evaluated during ordinary solves. \texttt{Draft} selects the Minimal column (few warnings, fast iteration); \texttt{Standard} and \texttt{Publication} select the Standard - column. In v0.1 no column tighter than Standard is ratified; + column. As of v0.2 no column tighter than Standard is ratified; \texttt{Publication} is registered now so that documents and configurations can name it, and a future revision \MAY{} give it a tighter column without a schema change. @@ -1302,6 +1320,18 @@ than inventing a parallel one. Open questions: the notated-but-unrendered honesty edge, optical spacing at the Standard tier, threshold tuning pending reference-suite experience. \\ + \today & \hyperref[sec:metrics:spacing]{\texttt{spacing\_distortion}} + & 0.2.0 --- Scope the \texttt{spacing\_distortion} raw measurement to the + system's \emph{rhythmic} columns (slots bearing a notehead or rest), + excluding the clef / key-signature / time-signature lead and treating + barlines transparently (a note-to-note advance spans them). Resolves the + reference-suite false positive in which a short healthy line's wide + clef-to-first-note gap inflated the CV above the Standard warning floor + (measured 0.36--0.41 on the three- to eight-column entries $\to$ + 0.08--0.22, below the floor) without weakening the axis on real spacing + irregularity. Normalization anchor, orientation, range, thresholds, and + the eight other axes are unchanged; the optical-spacing open question + (duration-proportional spacing) stays open. Batch item P12-I12. \\ \bottomrule \end{longtable}