""" Surf Evaluation Tool - Multi-Factor Scoring Algorithm. This module implements a sophisticated surf condition evaluation system that scores surf spots based on multiple environmental factors and user preferences. The algorithm considers wave conditions, wind patterns, swell direction, and skill compatibility. Scoring Components (weighted): - Wave Conditions (35%): Height matching and safety - Wind Analysis (25%): Speed and direction optimization - Swell Direction (25%): Angular matching to optimal window - Skill Compatibility (15%): Break type vs experience level The scoring system uses normalized 0-100 scales with progressive curves that reward optimal conditions while penalizing dangerous or suboptimal scenarios. Example: >>> evaluator = SurfEvaluatorTool() >>> result = await evaluator.run({ ... "spot": surf_spot_data, ... "conditions": current_wave_data, ... "prefs": {"skill_level": "intermediate"} ... }) >>> print(f"Score: {result['score']}/100") Author: Surf Spot Finder Team License: MIT """ from pydantic import BaseModel from typing import Dict, Any, List, Tuple import math class SurfEvaluatorTool: """ Advanced surf evaluation tool with multi-factor scoring algorithm. This tool implements a comprehensive evaluation system that analyzes surf conditions across multiple dimensions to produce a single score representing the quality and suitability of surfing conditions. The algorithm weighs different factors based on their importance: - Wave conditions: Safety and optimal height ranges - Wind patterns: Offshore vs onshore preferences - Swell direction: Alignment with spot's optimal angles - User skill level: Appropriate difficulty matching All scores are normalized to 0-100 scale for consistency. Attributes: name: Tool identifier for MCP registration. description: Human-readable tool description. Example: >>> evaluator = SurfEvaluatorTool() >>> input_data = SurfEvaluatorTool.SurfEvalInput( ... spot=spot_data, ... conditions=wave_conditions, ... prefs={"skill_level": "beginner"} ... ) >>> result = await evaluator.run(input_data.dict()) """ class SurfEvalInput(BaseModel): """Input schema for surf evaluation. Attributes: spot: Surf spot data including location and characteristics. conditions: Current wave/wind conditions from marine APIs. prefs: User preferences including skill level and board type. """ spot: Dict[str, Any] conditions: Dict[str, Any] prefs: Dict[str, Any] = {} def is_direction_in_range(self, direction: float, direction_range: List[float]) -> bool: """Check if direction falls within optimal range for the surf spot. Handles both normal ranges and those crossing 0° (e.g., 315-45°). Args: direction: Current direction in degrees (0-360). direction_range: [start, end] optimal range in degrees. Returns: True if direction is within the optimal range. """ if len(direction_range) != 2: return False start, end = direction_range # Handle ranges that cross 0° (e.g., [315, 45]) if start > end: return direction >= start or direction <= end else: return start <= direction <= end def calculate_direction_score(self, direction: float, optimal_range: List[float]) -> float: """Calculate score based on direction proximity to optimal range. Uses progressive scoring where perfect alignment = 1.0, and score decreases with angular distance from optimal range. Args: direction: Current direction in degrees. optimal_range: [start, end] optimal range in degrees. Returns: Score between 0.0-1.0 based on direction quality. """ if not optimal_range or len(optimal_range) != 2: return 0.5 # Neutral score if no preference if self.is_direction_in_range(direction, optimal_range): return 1.0 # Perfect score if in range start, end = optimal_range # Calculate distance to nearest edge of range if start > end: # Range crosses 0° dist_to_start = min(abs(direction - start), 360 - abs(direction - start)) dist_to_end = min(abs(direction - end), 360 - abs(direction - end)) else: dist_to_start = abs(direction - start) dist_to_end = abs(direction - end) min_distance = min(dist_to_start, dist_to_end) # Score decreases with distance (max penalty at 90° off) return max(0, 1 - (min_distance / 90)) def evaluate_wave_height(self, wave_height: float, spot_prefs: Dict[str, Any], user_prefs: Dict[str, Any]) -> Tuple[float, str]: """Evaluate wave height against spot and user preferences.""" min_height = spot_prefs.get("min_wave_height", 0.3) max_height = spot_prefs.get("max_wave_height", 10.0) # User skill level adjustments skill_level = user_prefs.get("skill_level", "intermediate") if skill_level == "beginner": max_height = min(max_height, 2.0) elif skill_level == "expert": min_height = max(min_height, 1.5) if wave_height < min_height: score = wave_height / min_height * 0.5 # Partial score for smaller waves explanation = f"Wave height {wave_height}m below minimum {min_height}m" elif wave_height > max_height: score = max(0, 1 - (wave_height - max_height) / max_height) explanation = f"Wave height {wave_height}m above comfortable maximum {max_height}m" else: # Optimal range - score based on how close to ideal ideal_height = (min_height + max_height) / 2 distance_from_ideal = abs(wave_height - ideal_height) range_size = max_height - min_height score = 1 - (distance_from_ideal / (range_size / 2)) * 0.3 # Max 30% penalty explanation = f"Wave height {wave_height}m in optimal range {min_height}-{max_height}m" return max(0, score), explanation def evaluate_wind(self, wind_speed: float, wind_direction: float, spot_prefs: Dict[str, Any]) -> Tuple[float, str]: """Evaluate wind conditions for surfing.""" optimal_wind_dir = spot_prefs.get("wind_direction", []) # Wind speed scoring (offshore/light winds preferred) if wind_speed <= 5: wind_speed_score = 1.0 elif wind_speed <= 15: wind_speed_score = 1 - ((wind_speed - 5) / 10) * 0.5 else: wind_speed_score = max(0, 0.5 - ((wind_speed - 15) / 20) * 0.5) # Wind direction scoring wind_dir_score = self.calculate_direction_score(wind_direction, optimal_wind_dir) # Combined wind score wind_score = (wind_speed_score + wind_dir_score) / 2 explanation = f"Wind {wind_speed}kt at {wind_direction}°" if optimal_wind_dir: explanation += f" (optimal: {optimal_wind_dir[0]}-{optimal_wind_dir[1]}°)" return wind_score, explanation def evaluate_swell(self, swell_direction: float, spot_prefs: Dict[str, Any]) -> Tuple[float, str]: """Evaluate swell direction compatibility.""" optimal_swell_dir = spot_prefs.get("swell_direction", []) if not optimal_swell_dir: return 0.7, "No swell direction preference specified" score = self.calculate_direction_score(swell_direction, optimal_swell_dir) explanation = f"Swell from {swell_direction}° (optimal: {optimal_swell_dir[0]}-{optimal_swell_dir[1]}°)" return score, explanation def calculate_skill_compatibility(self, spot: Dict[str, Any], user_prefs: Dict[str, Any]) -> Tuple[float, str]: """Check if spot matches user skill level.""" user_skill = user_prefs.get("skill_level", "intermediate") spot_skills = spot.get("characteristics", {}).get("skill_level", ["intermediate"]) if user_skill in spot_skills: score = 1.0 explanation = f"Perfect skill match for {user_skill} surfer" elif user_skill == "beginner" and "intermediate" in spot_skills: score = 0.7 explanation = "Spot suitable for progression to intermediate" elif user_skill == "intermediate" and "advanced" in spot_skills: score = 0.8 explanation = "Challenging spot for skill development" else: score = 0.3 explanation = f"Skill mismatch: {user_skill} vs {spot_skills}" return score, explanation async def run(self, input_data: Dict[str, Any]) -> Dict[str, Any]: """Run comprehensive surf spot evaluation.""" data = self.SurfEvalInput(**input_data) spot = data.spot conditions = data.conditions user_prefs = data.prefs spot_name = spot.get("name", "Unknown Spot") spot_prefs = spot.get("optimal_conditions", {}) # Extract conditions wave_height = conditions.get("wave_height", 0) wind_speed = conditions.get("wind_speed", 0) wind_direction = conditions.get("wind_direction", 0) swell_direction = conditions.get("swell_direction", 0) # Individual evaluations wave_score, wave_explanation = self.evaluate_wave_height(wave_height, spot_prefs, user_prefs) wind_score, wind_explanation = self.evaluate_wind(wind_speed, wind_direction, spot_prefs) swell_score, swell_explanation = self.evaluate_swell(swell_direction, spot_prefs) skill_score, skill_explanation = self.calculate_skill_compatibility(spot, user_prefs) # Weighted total score weights = { "wave": 0.35, "wind": 0.25, "swell": 0.25, "skill": 0.15 } total_score = ( wave_score * weights["wave"] + wind_score * weights["wind"] + swell_score * weights["swell"] + skill_score * weights["skill"] ) * 100 # Scale to 0-100 # Detailed explanation explanation = f""" Evaluation for {spot_name}: • Waves: {wave_explanation} (Score: {wave_score:.2f}) • Wind: {wind_explanation} (Score: {wind_score:.2f}) • Swell: {swell_explanation} (Score: {swell_score:.2f}) • Skill Match: {skill_explanation} (Score: {skill_score:.2f}) Overall conditions rating: {total_score:.1f}/100 """.strip() return { "spot": spot_name, "score": round(total_score, 1), "explanation": explanation, "breakdown": { "wave_score": round(wave_score, 2), "wind_score": round(wind_score, 2), "swell_score": round(swell_score, 2), "skill_score": round(skill_score, 2) } }