Spaces:
Sleeping
Sleeping
| """ | |
| 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) | |
| } | |
| } | |