""" Deep Squat rubric scorer — pure function, no model calls. FMS Deep Squat Criteria: - Score 3: femur below horizontal, torso parallel to tibia, knees tracking over feet, dowel over feet, heels flat. - Score 2: criteria met only with heels elevated. - Score 1: criteria unmet even with heels elevated. - Score 0: PAIN — never auto-scored by this function. Input: BiomechFeatures for deep_squat Output: ScoreResult(score, rationale, confidence, needs_human) """ from __future__ import annotations import math from formscout.types import BiomechFeatures, ScoreResult from formscout import config def score_deep_squat(features: BiomechFeatures) -> ScoreResult: """ Pure rubric scorer for deep squat. Returns ScoreResult with score 1-3 based on biomechanical measurements. Never assigns score 0 (pain) — that requires needs_human=True from JudgeAgent. """ angles = features.angles alignments = features.alignments # Check if we have enough data to score has_femur = any( k in angles for k in ("left_femur_from_horizontal_deg", "right_femur_from_horizontal_deg") ) has_torso_tibia = "torso_tibia_angle_deg" in angles if not has_femur: return ScoreResult( score=1, rationale="Insufficient data: femur angle not measurable", confidence=0.3, needs_human=False, notes="missing femur measurements — defaulting to lowest passing score", ) # Evaluate criteria # Femur below horizontal: femur angle from horizontal > 90° means above horizontal # In our measurement: angle is from horizontal, so < 90 means below horizontal femur_angles = [] if "left_femur_from_horizontal_deg" in angles: femur_angles.append(angles["left_femur_from_horizontal_deg"]) if "right_femur_from_horizontal_deg" in angles: femur_angles.append(angles["right_femur_from_horizontal_deg"]) # Femur below horizontal means the thigh slopes down steeply (angle > ~60° from horizontal in image coords) femur_below_horizontal = any(a > 60.0 for a in femur_angles) if femur_angles else False # Torso parallel to tibia torso_parallel_tibia = ( angles.get("torso_tibia_angle_deg", 999) <= config.DEEP_SQUAT_TORSO_TIBIA_MAX_DEG ) # Knee tracking knees_tracking = alignments.get("knees_tracking_over_feet", False) # Dowel alignment dowel_over_feet = alignments.get("dowel_over_feet", False) # Heels heels_elevated = alignments.get("heels_elevated", False) # Scoring logic all_criteria = femur_below_horizontal and torso_parallel_tibia and knees_tracking and dowel_over_feet rationale_parts: list[str] = [] if all_criteria and not heels_elevated: score = 3 rationale_parts.append("All criteria met with heels flat") elif all_criteria and heels_elevated: score = 2 rationale_parts.append("Criteria met only with heels elevated") else: # Check what failed if not femur_below_horizontal: rationale_parts.append("femur not below horizontal") if not torso_parallel_tibia: rationale_parts.append( f"torso-tibia angle {angles.get('torso_tibia_angle_deg', '?')}° " f"exceeds {config.DEEP_SQUAT_TORSO_TIBIA_MAX_DEG}° threshold" ) if not knees_tracking: rationale_parts.append("knees not tracking over feet") if not dowel_over_feet: rationale_parts.append("dowel not aligned over feet") if heels_elevated: score = 1 rationale_parts.append("criteria unmet even with heels elevated") else: # They might score 2 with heel elevation — but without it, still 1 score = 1 rationale_parts.append("criteria unmet with heels flat") confidence = features.confidence * (0.9 if has_torso_tibia else 0.6) return ScoreResult( score=score, rationale="; ".join(rationale_parts), confidence=confidence, needs_human=False, notes="", )