From 56fa48061073485010092c91c8c0788ee55258a9 Mon Sep 17 00:00:00 2001 From: Mitchell Hansen Date: Sat, 2 May 2026 00:56:45 -0700 Subject: [PATCH] brush-paint-opt: drop barrier penalties from meta-opt inner score MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 100M-magnitude barriers in score_for_letter_with_weights swamped all soft-weight differences — so two different ScoreWeights candidates produced inner-descent results dominated by "minimise barriers", and the meta search couldn't tell them apart. Both candidates converged to identical PaintParams and identical CorpusReports, making the outer ranking meaningless. Drop the barriers. Inner descent is now purely guided by the candidate's ScoreWeights → different weights produce different optima. The OUTER lex comparator (compare_reports) handles tier-1 filtering at the end. Stroke-count penalties stay (per-letter natural-form requirements, 50k each) and the "refuse zero strokes" pin stays (200k) — without that the inner descent can degenerate to "paint nothing" under low coverage weight. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/brush_paint_opt.rs | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/brush_paint_opt.rs b/src/brush_paint_opt.rs index 5d9a63be..2190c293 100644 --- a/src/brush_paint_opt.rs +++ b/src/brush_paint_opt.rs @@ -446,27 +446,30 @@ pub fn evaluate_score_weights( (best_params, report) } -/// Same shape as score_for_letter but takes ScoreWeights so the meta- -/// optimizer can vary them. Mirrors the original `score_for_letter` -/// barriers exactly; the only thing the meta optimizer changes is the -/// soft-term weights (ScoreWeights), not the hard barriers. +/// Inner score function used during META-OPTIMIZATION. Unlike +/// `score_for_letter` (the production score), this version DOES NOT +/// add 100M-magnitude barriers for bg / coverage / length-budget +/// violations. +/// +/// Why: the barriers are so large they swamp every soft-weight +/// difference. With barriers in place, two different ScoreWeights +/// candidates produce inner-descent results dominated by "minimise +/// barriers" rather than "minimise the weighted soft score" — so +/// the inner optimizer converges to identical PaintParams under +/// most weight choices and the meta search has nothing to compare. +/// +/// Without barriers, the inner descent is purely guided by the +/// candidate's ScoreWeights → different weights produce genuinely +/// different optima → the OUTER lex comparator ranks them by the +/// hard criteria (tier-1 fail counts) at the end. +/// +/// Stroke-count penalties stay (they're per-letter natural-form +/// requirements, not score-vs-feasibility tradeoffs) and the +/// "refuse zero strokes" pin stays (without it the inner descent +/// can degenerate to "paint nothing" under low coverage weight). fn score_for_letter_with_weights(ch: char, m: &PaintMetrics, w: &ScoreWeights) -> f32 { use crate::brush_paint::score_weighted; let mut s = score_weighted(m, *w); - if m.total_swept > 0 { - let bg_rate = m.bg_painted as f32 / m.total_swept as f32; - if bg_rate > 0.05 { - s += 100_000_000.0 * (bg_rate - 0.05); - } - } - let cluster_threshold = 0.5 * std::f32::consts::PI * m.brush_radius * m.brush_radius; - let max_cluster = m.unpainted_clusters.iter().copied().max().unwrap_or(0) as f32; - if max_cluster > cluster_threshold { - s += 1_000_000.0 * (max_cluster - cluster_threshold); - } - if m.skeleton_length > 0 && m.total_length > 2.0 * m.skeleton_length as f32 { - s += 100_000.0 * (m.total_length - 2.0 * m.skeleton_length as f32); - } if m.strokes == 0 { s += 200_000.0; } if is_single_stroke_letter(ch) && m.strokes != 1 { s += 50_000.0 * ((m.strokes as i64 - 1).abs() as f32);