D3MI4N's picture
docs: add comprehensive docstrings to all Python files
23a9367
"""
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)
}
}