"""Computes the deterministic difficulty score from post-render geometry metrics. Pure function of :class:`~src.analysis.geometry_analyzer.GeometryResult` and the configured weights -- no randomness is involved, so the score is trivially reproducible given identical geometry inputs. """ from __future__ import annotations from dataclasses import dataclass import numpy as np from src.analysis.geometry_analyzer import GeometryResult from src.config.schema import DifficultyWeights from src.utils.geometry_utils import clamp01 @dataclass class DifficultyResult: """The five difficulty components, the weighted score, and its tier.""" misplacement: float whitespace: float faintness: float smallness: float low_texture: float score: float tier: str class DifficultyAnalyzer: """Computes the exact spec-defined difficulty score and tier. ``score = 0.35*misplacement + 0.20*whitespace + 0.20*faintness + 0.15*smallness + 0.10*low_texture`` Example: >>> analyzer = DifficultyAnalyzer(cfg.difficulty) >>> result = analyzer.analyze(geometry_result) """ def __init__(self, weights: DifficultyWeights) -> None: self.weights = weights def analyze(self, geometry: GeometryResult) -> DifficultyResult: """Compute the difficulty components, weighted score, and tier for one sample.""" w = self.weights misplacement = clamp01(1.0 - geometry.polygon_overlap_proxy) whitespace = clamp01(geometry.whitespace_ratio) faintness = clamp01((geometry.mean_dark_intensity - 90.0) / 120.0) smallness = clamp01(1.0 - geometry.signature_area_ratio / 0.04) low_texture = clamp01(1.0 - geometry.gray_stddev / 60.0) score = ( w.misplacement * misplacement + w.whitespace * whitespace + w.faintness * faintness + w.smallness * smallness + w.low_texture * low_texture ) score = float(np.clip(score, 0.0, 1.0)) if score < w.easy_threshold: tier = "easy" elif score < w.hard_threshold: tier = "medium" else: tier = "hard" return DifficultyResult( misplacement=round(misplacement, 4), whitespace=round(whitespace, 4), faintness=round(faintness, 4), smallness=round(smallness, 4), low_texture=round(low_texture, 4), score=round(score, 4), tier=tier, )