fix: report renders empty-truth roles as n/a with spurious count
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
42d88a2401
commit
d9fd5d8c9f
+21
-11
@@ -29,19 +29,25 @@ function stat(xs: (number | null | undefined)[], d = 2): string {
|
||||
return `${fmt(median(v), d)} (${fmt(Math.min(...v), d)}–${fmt(Math.max(...v), d)})`;
|
||||
}
|
||||
|
||||
type Col = { head: string; get: (s: SongScore) => number | null; d?: number };
|
||||
const drum = (c: "kick" | "snare" | "hihat") => (s: SongScore) => s.drumsPerClass?.[c].f1 ?? null;
|
||||
type M = { tp: number; fp: number; fn: number; f1: number | null } | null | undefined;
|
||||
/** F1 only when the truth has notes for the role; otherwise n/a (null). */
|
||||
const f1OrNA = (m: M): number | null => (m && m.tp + m.fn > 0 ? m.f1 : null);
|
||||
/** Spurious count (fp) when the truth is empty for the role. */
|
||||
const spurOf = (m: M): number => (m && m.tp + m.fn === 0 ? m.fp : 0);
|
||||
type Col = { head: string; get: (s: SongScore) => number | null; spur?: (s: SongScore) => number; d?: number };
|
||||
const roleCol = (head: string, pick: (s: SongScore) => M): Col => ({ head, get: (s) => f1OrNA(pick(s)), spur: (s) => spurOf(pick(s)) });
|
||||
const drum = (head: string, c: "kick" | "snare" | "hihat") => roleCol(head, (s) => s.drumsPerClass?.[c]);
|
||||
const COLS: Col[] = [
|
||||
{ head: "Guitar merged F1", get: (s) => s.guitarMerged.f1 },
|
||||
{ head: "Guitar octave errors", get: (s) => s.guitarMerged.octaveErrors, d: 0 },
|
||||
{ head: "Guitar octave errors", get: (s) => s.guitarMerged.octaveErrors, d: 1 },
|
||||
{ head: "Spurious rate", get: (s) => s.spuriousRate },
|
||||
{ head: "Part-split F1 (t1d)", get: (s) => s.partSplit?.f1 ?? null },
|
||||
{ head: "Vocals F1", get: (s) => s.perRole.vocals?.f1 ?? null },
|
||||
{ head: "Bass F1", get: (s) => s.perRole.bass?.f1 ?? null },
|
||||
{ head: "Drums kick F1", get: drum("kick") },
|
||||
{ head: "Drums snare F1", get: drum("snare") },
|
||||
{ head: "Drums hihat F1", get: drum("hihat") },
|
||||
{ head: "Keys F1", get: (s) => s.perRole.keys?.f1 ?? null },
|
||||
roleCol("Vocals F1", (s) => s.perRole.vocals),
|
||||
roleCol("Bass F1", (s) => s.perRole.bass),
|
||||
drum("Drums kick F1", "kick"),
|
||||
drum("Drums snare F1", "snare"),
|
||||
drum("Drums hihat F1", "hihat"),
|
||||
roleCol("Keys F1", (s) => s.perRole.keys),
|
||||
{ head: "Rhythm onBeat", get: (s) => s.rhythm?.onBeat ?? null },
|
||||
{ head: "Rhythm durationOk", get: (s) => s.rhythm?.durationOk ?? null },
|
||||
{ head: "Tempo factor", get: (s) => s.rhythm?.factor ?? null },
|
||||
@@ -49,6 +55,10 @@ const COLS: Col[] = [
|
||||
{ head: "String agreement", get: (s) => s.stringAgreement },
|
||||
];
|
||||
|
||||
const withSpur = (cell: string, c: Col, scores: SongScore[]) => {
|
||||
const n = c.spur ? scores.reduce((a, x) => a + c.spur!(x), 0) : 0;
|
||||
return cell === NA && n > 0 ? `${NA} (${n} spurious)` : cell;
|
||||
};
|
||||
const row = (cells: string[]) => `| ${cells.join(" | ")} |`;
|
||||
const table = (head: string[], rows: string[][]) =>
|
||||
[row(head), row(head.map(() => "---")), ...rows.map(row)].join("\n");
|
||||
@@ -74,7 +84,7 @@ export function renderReport(run: BenchRun): string {
|
||||
const ms = rs.filter((r) => r.mode === m.name);
|
||||
const guitarCell = `${stat(ms.map((r) => r.score.guitarMerged.f1))} ${ms.every((r) => r.score.guitarMerged.pass) ? "✓" : "✗"}`;
|
||||
return [m.name, String(ms.length), guitarCell,
|
||||
...COLS.slice(1).map((c) => stat(ms.map((r) => c.get(r.score)), c.d ?? 2))];
|
||||
...COLS.slice(1).map((c) => withSpur(stat(ms.map((r) => c.get(r.score)), c.d ?? 2), c, ms.map((r) => r.score)))];
|
||||
});
|
||||
out.push(table(["Mode", "Songs", ...COLS.map((c) => c.head)], rows), "");
|
||||
const full = rs.filter((r) => r.mode === "full");
|
||||
@@ -88,6 +98,6 @@ export function renderReport(run: BenchRun): string {
|
||||
const sorted = [...run.results].sort((a, b) => a.tier.localeCompare(b.tier) || a.seed - b.seed || a.mode.localeCompare(b.mode));
|
||||
out.push(table(["Tier", "Seed", "Mode", "Guitar pass", ...COLS.map((c) => c.head), "Seconds"],
|
||||
sorted.map((r) => [r.tier, String(r.seed), r.mode, r.score.guitarMerged.pass ? "✓" : "✗",
|
||||
...COLS.map((c) => fmt(c.get(r.score), c.d ?? 2)), r.seconds.toFixed(1)])), "");
|
||||
...COLS.map((c) => withSpur(fmt(c.get(r.score), c.d ?? 2), c, [r.score])), r.seconds.toFixed(1)])), "");
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
@@ -31,8 +31,9 @@ const mk = (g: number): BenchRun => ({
|
||||
describe("renderReport", () => {
|
||||
test("column order, n/a, caveat", () => {
|
||||
const md = renderReport(mk(0.9));
|
||||
expect(md.indexOf("Guitar")).toBeLessThan(md.indexOf("Vocals"));
|
||||
expect(md.indexOf("Vocals")).toBeLessThan(md.indexOf("Bass"));
|
||||
const header = md.split("\n").find((l) => l.startsWith("| Mode"))!;
|
||||
expect(header.indexOf("Guitar merged F1")).toBeLessThan(header.indexOf("Vocals F1"));
|
||||
expect(header.indexOf("Vocals F1")).toBeLessThan(header.indexOf("Bass F1"));
|
||||
expect(md).toContain("n/a");
|
||||
expect(md).toContain("Voice Oohs");
|
||||
expect(md).toContain("fx");
|
||||
@@ -43,4 +44,12 @@ describe("renderReport", () => {
|
||||
expect(bad).toContain("FAIL");
|
||||
expect(bad).not.toContain("Tier t1a: PASS");
|
||||
});
|
||||
test("role with empty truth renders n/a with spurious count", () => {
|
||||
const run = mk(0.9);
|
||||
const s = run.results[0]!.score;
|
||||
s.perRole.bass = { ...mr(0), tp: 0, fp: 3, fn: 0 };
|
||||
const md = renderReport({ ...run, results: [run.results[0]!] });
|
||||
expect(md).toContain("n/a (3 spurious)");
|
||||
expect(md).not.toContain("0.00 (0.00");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user