"""Ring size recommendation from calibrated finger width.""" from typing import Dict, List, Literal, Optional, Tuple # Ring model definitions: model name → {size: inner_diameter_mm} RING_MODELS: Dict[str, Dict[int, float]] = { "gen": { 6: 16.9, 7: 17.7, 8: 18.6, 9: 19.4, 10: 20.3, 11: 21.1, 12: 21.9, 13: 22.7, }, "air": { 6: 16.6, 7: 17.4, 8: 18.2, 9: 19.0, 10: 19.9, 11: 20.7, 12: 21.5, 13: 22.3, }, } VALID_RING_MODELS = list(RING_MODELS.keys()) DEFAULT_RING_MODEL = "gen" # Backwards-compatible alias RING_SIZE_CHART = RING_MODELS[DEFAULT_RING_MODEL] def _get_sorted_sizes(ring_model: str) -> List[Tuple[int, float]]: chart = RING_MODELS.get(ring_model, RING_MODELS[DEFAULT_RING_MODEL]) return sorted(chart.items(), key=lambda x: x[1]) def recommend_ring_size(diameter_cm: float, ring_model: str = DEFAULT_RING_MODEL) -> Optional[Dict]: """Recommend ring size from calibrated finger outer diameter. Returns dict with: - best_match: nearest ring size (int) - best_match_inner_mm: inner diameter of best match - range_min / range_max: recommended 2-size range - diameter_mm: input converted to mm - ring_model: which model chart was used Returns None if diameter is out of reasonable range. """ diameter_mm = diameter_cm * 10.0 if diameter_mm < 14.0 or diameter_mm > 26.0: return None sorted_sizes = _get_sorted_sizes(ring_model) # Find nearest size best_size, best_inner = min(sorted_sizes, key=lambda x: abs(x[1] - diameter_mm)) # Find second nearest size second_size, second_inner = min( (s for s in sorted_sizes if s[0] != best_size), key=lambda x: abs(x[1] - diameter_mm), ) range_min = min(best_size, second_size) range_max = max(best_size, second_size) return { "best_match": best_size, "best_match_inner_mm": best_inner, "range_min": range_min, "range_max": range_max, "diameter_mm": round(diameter_mm, 2), "ring_model": ring_model, } def aggregate_ring_sizes(per_finger_results: Dict[str, Dict]) -> Dict: """Aggregate ring size recommendations from multiple fingers. Args: per_finger_results: Dict mapping finger name to measurement result dict. Each value must have keys: - "finger_outer_diameter_cm": float or None - "confidence": float - "ring_size": dict from recommend_ring_size() or None - "fail_reason": str or None Returns: Dict with: - overall_best_size: int (consensus size if one exists in all fingers' ranges, otherwise confidence-weighted best size) - overall_range_min: int (min of all per-finger range_min) - overall_range_max: int (max of all per-finger range_max) - fingers_measured: int (total attempted) - fingers_succeeded: int (with valid measurement) - per_finger: dict of per-finger details - fail_reason: str or None (only if ALL fingers failed) """ fingers_measured = len(per_finger_results) # Build per_finger summary per_finger: Dict[str, Dict] = {} for name, result in per_finger_results.items(): failed = result.get("fail_reason") is not None or result.get("ring_size") is None rs = result.get("ring_size") per_finger[name] = { "diameter_cm": result.get("finger_outer_diameter_cm"), "confidence": result.get("confidence", 0.0), "best_match": rs["best_match"] if rs else None, "range": [rs["range_min"], rs["range_max"]] if rs else None, "status": "failed" if failed else "ok", "fail_reason": result.get("fail_reason"), } # Filter to succeeded fingers succeeded = { name: info for name, info in per_finger.items() if info["status"] == "ok" } if not succeeded: return { "fail_reason": "all_fingers_failed", "fingers_measured": fingers_measured, "fingers_succeeded": 0, "per_finger": per_finger, } # Confidence-weighted voting for best size vote_tally: Dict[int, float] = {} for info in succeeded.values(): size = info["best_match"] vote_tally[size] = vote_tally.get(size, 0.0) + info["confidence"] weighted_best_size = max(vote_tally, key=lambda s: vote_tally[s]) # Intersection-first override: if a size falls in every finger's range, prefer it all_ranges = [set(range(info["range"][0], info["range"][1] + 1)) for info in succeeded.values()] consensus_sizes = set.intersection(*all_ranges) if all_ranges else set() if consensus_sizes: # Pick the consensus size closest to the confidence-weighted winner overall_best_size = min(consensus_sizes, key=lambda s: abs(s - weighted_best_size)) else: overall_best_size = weighted_best_size # Aggregate range overall_range_min = min(info["range"][0] for info in succeeded.values()) overall_range_max = max(info["range"][1] for info in succeeded.values()) # Ensure range covers best size if overall_best_size < overall_range_min: overall_range_min = overall_best_size if overall_best_size > overall_range_max: overall_range_max = overall_best_size return { "overall_best_size": overall_best_size, "overall_range_min": overall_range_min, "overall_range_max": overall_range_max, "fingers_measured": fingers_measured, "fingers_succeeded": len(succeeded), "per_finger": per_finger, "fail_reason": None, }