Skip to content

lab: fingering tie-break — exact optimum sets and a learned lexicographic secondary - #199

Open
PhysShell wants to merge 9 commits into
claude/lab-optimization-gapfrom
claude/lab-fingering-tie-break
Open

PhysShell wants to merge 9 commits into
claude/lab-optimization-gapfrom
claude/lab-fingering-tie-break

Conversation

@PhysShell

Copy link
Copy Markdown
Owner

What

Follow-up to #197. Under the fitted v1 weights the production DP keeps 44.1% agreement with tab authors on holdout songs, while the set of cost-optimal fingerings contains 55.5%. That gap needs no new primary objective, only a better choice among ties. This PR measures the tie sets exactly, learns a human-blind lexicographic tie-break, and locates the information the authors used.

Stacked on #197 (base claude/lab-optimization-gap): the diff shows only this work. Retarget to main once #197 is merged.

Research tooling only (lab/, not a workspace member); no production code changes.

Built (red → green per commit)

  • lab/src/ties.rs:
    • optimum_set — exact chain DPs: optimal-path count (saturating u64 + exact ln), least / most / uniform-expected agreement among optimal paths, a most-agreeing optimal path;
    • lexicographic_path — primary, then secondary; zero weights reproduce the production path exactly; optional loss augmentation;
    • latent_target — cheapest most-agreeing optimal path under current weights;
    • train_secondary — averaged, loss-augmented, latent-target perceptron on integer weights;
    • 20 local geometry features + anchor_distance.
  • TabLine::anchor_fret — the fret of the latest fretted note before a line (lowest at a chord onset).
  • fingering_gap ties-check (DPs vs verified CP-SAT records) and tiebreak (song-level fit/validation/test, margin chosen on validation, local vs local + anchor ablation).

Results (holdout songs, primary v1-fit)

rung agreement
floor (least-agreeing optimal path) 32.4%
uniform draw from the optimum set 42.9%
production tie-break 44.1%
learned tie-break, local features 44.0%
learned tie-break, local + hand anchor 47.3% (+3.2 pt, 28% of the gap)
ceiling (most-agreeing optimal path) 55.5%
  • Exact DPs = CP-SAT on 1,954 / 1,954 holdout lines for v1 and v1-fit, both optimum and ceiling. The ceiling now costs milliseconds.
  • Local geometry carries no tie information. The learner converges on production's own choices. Where the author deviates, it is always one or two strings lower and higher up the neck, yet the author agrees with production in ~75% of lines.
  • The information is contextual. The author's choice is closer to the fret of the preceding fretted event in 63% of differing lines (production's in 33%). Adding that one anchor feature produced the only gain.
  • Control: with production v1 weights the optimum is unique in 87% of lines, the whole ladder spans 35.7–36.2%, and the learned tie-break sits inside it at 36.1%.

Full write-up: docs/audit/2026-09-fingering-tie-break.md.

Limitations

  • The anchor comes from the human tab (tab completion). MIDI-sourced lines need chord voicing to get one — the next Lab subject.
  • Linear hand-featured secondary; the perceptron does not converge on real data (averaged weights, 20 epochs).

Checks

  • cd lab && cargo test: 96 tests green (17 ties, 30 fingering, 13 optir, 24 oracle spike, 12 propagate), including brute-force contracts and zero-secondary equality with infer_positions.
  • cargo fmt --check clean; clippy clean at the crate's deny level.

🤖 Generated with Claude Code

The optimality-gap audit (#197) found the fitted v1 objective
under-discriminative: on holdout songs the DP's fixed tie-break keeps 44.1%
agreement with tab authors while the optimum set contains 55.5%. This
opens the follow-up: measure that set exactly with chain DPs instead of an
external solver, and learn a human-blind tie-break inside it.

Pins, against brute force on exhaustive small families:
- Chain::v1 mirrors the production objective path by path;
- optimum_set: optimum, exact path count (saturating u64 with an exact
  natural log), and least / most / expected agreement with a reference
  among optimal paths, plus a most-agreeing optimal path — the achievable
  learning target;
- lexicographic_path is primary- then secondary-optimal, reproduces the
  production DP path exactly with zero secondary weights, and with loss
  augmentation finds the least-agreeing optimal path;
- path_features sums the per-note and per-transition secondary features;
- train_secondary (averaged, loss-augmented structured perceptron over
  integer weights) converges on a separable tie-break, generalizes to
  held-out lines, is deterministic, and makes no update when the target is
  already chosen.

All new functions are todo!() stubs (13 red); the existing 78 lab tests
stay green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ie-break

ties::Chain holds the v1 objective per line (candidates in production
order, unary and pairwise costs). optimum_set solves forward and backward
cost tables with path counts (saturating u64 plus an exact natural log via
log-add), marks nodes and edges on optimal paths by f + g == optimum, and
reads off: the optimal-path count; least and most reference agreement by
min/max DPs over optimal edges, with a most-agreeing optimal path as the
achievable learning target; and the expected agreement under a uniform draw
from the optimum set (paths through a candidate / total, in log space).

lexicographic_path minimizes (primary i64, secondary i128) with strict
comparisons, so zero secondary weights reproduce the production DP path
exactly; loss augmentation adds a margin per reference-matching note.
train_secondary is an averaged structured perceptron over integer weights
inside the primary optimum set, updating toward the achievable target and
stopping after an epoch without updates.

Contract suite green (13 ties + 78 existing); clippy clean at the crate's
deny level (pedantic too_many_lines warnings only).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ties-check compares the exact optimum-set DPs with verified CP-SAT records
(optimum and agreement ceiling per line). tiebreak reports the tie-break
ladder for a v1 primary — floor, uniform over optima, production tie-break,
learned tie-break, ceiling — plus optimal-path counts and unique-optimum
lines; it trains the secondary on train songs with a song-level validation
bucket for the margin and evaluates on holdout songs. Lines carry their
holdout bucket, and the ladder counts lines where the learned tie-break
changes the production path.

First runs on the corrected corpus: the exact DPs agree with CP-SAT on all
1,954 holdout lines for v1 and v1-fit (optimum and ceiling). The learned
tie-break changed 0 of 7,091 train lines — traced to the training setup,
not the DP (fixed targets conflict across lines; margins far below the
weight scale), addressed next.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first tie-break run learned weights that changed 0 of 7,091 train
lines. One cause is the fixed target: best_path breaks ties among the
most-agreeing optimal paths by candidate index, so equally good targets
disagree across similar lines and the perceptron chases conflicting
updates. The standard remedy is a latent target — among the optimal paths
that agree most with the tab author, the one cheapest under the current
secondary weights.

Pins latent_target against brute force: primary-optimal, most agreeing,
then least secondary cost; equal to best_path under zero weights; refused
for a reference of another length. Stub is todo!() (1 red).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lexicographic_path and latent_target share one DP minimizing
(primary, middle, secondary) with strict comparisons: Plain ignores the
reference, Augment adds the margin to the secondary per matching note,
Latent puts −matches in the middle so the most-agreeing optimal paths win
before the secondary cost. Zero weights still reproduce the production
path, and the latent target under zero weights equals best_path.

train_secondary now updates toward the latent target under the current
weights instead of a fixed best_path, removing conflicting targets across
lines with equally good choices.

Suite green (14 ties + 78 existing); clippy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With the latent target and margins up to 10,000, the learned tie-break
still changed only 7 of 7,091 train lines. Diagnosis on train songs: where
a most-agreeing optimal path differs from production, the tab author
plays the same pitch one or two strings lower and higher up the neck
(+1 string/+5 frets 50%, +1/+4 40%, +2/+9 7%) — yet in ~75% of lines the
author agrees with production, so no global local-geometry weight
separates the cases; 42.5% of the differing notes sit in repeated-pitch
runs, where travel inside the line is free. The deciding information lies
outside the line: the author's choice is closer to the fret of the last
fretted event before the line in 63% of differing lines (production's in
33%).

Pins that context as a measured feature:
- TabLine::anchor_fret — the fret of the latest positioned, fretted note of
  the voice before the line (lowest fret at a chord onset; open strings and
  unpositioned notes skipped; None at a voice's start);
- Chain::with_anchor / anchor and the anchor_distance secondary feature
  (|fret − anchor| per fretted note), primary cost unchanged;
- brute-force secondary optimality with anchored chains, and a perceptron
  that learns an anchor-nearest tie-break on held-out lines.

TabLine carries anchor_fret: None and with_anchor is todo!() here (4 red).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…feature

tab_lines tracks, per voice, the lowest fretted position at the latest
onset seen (chord notes included, open strings and unpositioned notes
skipped) and stamps it on a line when its first note arrives: the fret the
hand was at just before the line. Chain::with_anchor keeps the primary
objective and feeds anchor_distance (|fret - anchor| per fretted note) to
the secondary features.

Suite green (30 fingering + 17 ties + 49 spike/optir); clippy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…chor

tiebreak now trains and reports two secondary variants per primary: local
fingering-geometry features only, and the same plus anchor_distance to the
line's hand anchor. Margins span 0 to 10^7 and training runs 20 epochs;
the margin is chosen on the song-level validation bucket per variant, and
the ladder reports lines whose path the learned tie-break changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Archives the optimum-set and learned tie-break results (aggregates only):
exact DPs verified against CP-SAT optima and ceilings on 1,954 / 1,954
holdout lines; the tie-break ladder under v1-fit — floor 32.4%, uniform
42.9%, production 44.1%, learned local 44.0%, learned local + hand anchor
47.3%, ceiling 55.5%; tie structure and the v1 control; the diagnosis that
tab authors' deviations are one-directional (lower string, higher fret)
but context-dependent, with the preceding fretted event closer to the
author's choice in 63% of differing lines; limitations (human-sourced
anchor, linear non-convergent learner) and follow-ups.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: de3901bd-477f-4605-82e7-6b2d9ac532f3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants