mirror of
https://github.com/ChrisSewell/LeosShoes.git
synced 2025-07-01 10:07:27 -04:00
Enhancement: Advanced surface recovery modeling - Added configurable threshold, graduated recovery scoring, surface type models, time-of-day factors, and sun exposure effects with test scripts and documentation
This commit is contained in:
27
README.md
27
README.md
@ -148,8 +148,35 @@ UV_THRESHOLD_HIGH=10
|
|||||||
RISK_THRESHOLD_SHOES=6
|
RISK_THRESHOLD_SHOES=6
|
||||||
ROLLING_WINDOW_HOURS=2
|
ROLLING_WINDOW_HOURS=2
|
||||||
SURFACE_RECOVERY_HOURS=2
|
SURFACE_RECOVERY_HOURS=2
|
||||||
|
|
||||||
|
# Surface recovery enhancements (Optional)
|
||||||
|
SURFACE_RECOVERY_TEMP_THRESHOLD=90 # Temperature threshold to start recovery period (°F)
|
||||||
|
SURFACE_TYPE=asphalt # Options: asphalt, concrete, mixed, grass
|
||||||
|
SURFACE_MAX_RECOVERY_SCORE=2.0 # Maximum recovery score reduction with graduated recovery
|
||||||
|
ENABLE_GRADUATED_RECOVERY=true # Enable graduated recovery (more cooling time = more benefit)
|
||||||
|
ENABLE_TIME_OF_DAY_FACTOR=true # Apply day/night cooling adjustments
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Surface Recovery Feature
|
||||||
|
|
||||||
|
The application includes an enhanced surface recovery model that accounts for cooling of surfaces after peak temperatures:
|
||||||
|
|
||||||
|
- **Basic Recovery**: Surfaces begin to cool after being exposed to high temperatures (above `SURFACE_RECOVERY_TEMP_THRESHOLD`). After `SURFACE_RECOVERY_HOURS`, a recovery credit reduces the risk score.
|
||||||
|
|
||||||
|
- **Graduated Recovery**: When enabled, provides a progressively stronger recovery benefit as more cooling time passes, up to `SURFACE_MAX_RECOVERY_SCORE`.
|
||||||
|
|
||||||
|
- **Surface Type**: Different surfaces cool at different rates:
|
||||||
|
- Asphalt: Slowest cooling (coefficient 0.7)
|
||||||
|
- Concrete: Moderate cooling (coefficient 0.85)
|
||||||
|
- Mixed: Standard cooling (coefficient 1.0)
|
||||||
|
- Grass: Fastest cooling (coefficient 1.5)
|
||||||
|
|
||||||
|
- **Time-of-Day**: When enabled, night hours (7pm-6am) provide 30% faster cooling than daylight hours.
|
||||||
|
|
||||||
|
- **Sun Exposure**: The algorithm considers sun exposure during the recovery period, which can slow cooling by up to 30%.
|
||||||
|
|
||||||
|
These factors combine to provide a more accurate assessment of surface temperatures throughout the day and the resulting paw burn risk.
|
||||||
|
|
||||||
## Output Examples
|
## Output Examples
|
||||||
|
|
||||||
### Summary Output
|
### Summary Output
|
||||||
|
15
config.py
15
config.py
@ -23,6 +23,13 @@ class RiskConfig:
|
|||||||
rolling_window_hours: int = 2
|
rolling_window_hours: int = 2
|
||||||
surface_recovery_hours: int = 2
|
surface_recovery_hours: int = 2
|
||||||
|
|
||||||
|
# Surface recovery enhancement parameters
|
||||||
|
surface_recovery_temp_threshold: float = 90.0
|
||||||
|
surface_type: str = "asphalt" # Options: asphalt, concrete, mixed, grass
|
||||||
|
surface_max_recovery_score: float = 2.0
|
||||||
|
enable_graduated_recovery: bool = True
|
||||||
|
enable_time_of_day_factor: bool = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_env(cls) -> 'RiskConfig':
|
def from_env(cls) -> 'RiskConfig':
|
||||||
"""Create configuration from environment variables."""
|
"""Create configuration from environment variables."""
|
||||||
@ -35,7 +42,13 @@ class RiskConfig:
|
|||||||
uv_threshold_high=float(os.getenv('UV_THRESHOLD_HIGH', 10)),
|
uv_threshold_high=float(os.getenv('UV_THRESHOLD_HIGH', 10)),
|
||||||
risk_threshold_shoes=float(os.getenv('RISK_THRESHOLD_SHOES', 6)),
|
risk_threshold_shoes=float(os.getenv('RISK_THRESHOLD_SHOES', 6)),
|
||||||
rolling_window_hours=int(os.getenv('ROLLING_WINDOW_HOURS', 2)),
|
rolling_window_hours=int(os.getenv('ROLLING_WINDOW_HOURS', 2)),
|
||||||
surface_recovery_hours=int(os.getenv('SURFACE_RECOVERY_HOURS', 2))
|
surface_recovery_hours=int(os.getenv('SURFACE_RECOVERY_HOURS', 2)),
|
||||||
|
# Surface recovery enhancement parameters
|
||||||
|
surface_recovery_temp_threshold=float(os.getenv('SURFACE_RECOVERY_TEMP_THRESHOLD', 90)),
|
||||||
|
surface_type=os.getenv('SURFACE_TYPE', 'asphalt'),
|
||||||
|
surface_max_recovery_score=float(os.getenv('SURFACE_MAX_RECOVERY_SCORE', 2.0)),
|
||||||
|
enable_graduated_recovery=os.getenv('ENABLE_GRADUATED_RECOVERY', 'true').lower() == 'true',
|
||||||
|
enable_time_of_day_factor=os.getenv('ENABLE_TIME_OF_DAY_FACTOR', 'true').lower() == 'true'
|
||||||
)
|
)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
17
constants.py
Normal file
17
constants.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
"""Constants used throughout the application."""
|
||||||
|
|
||||||
|
# Surface cooling coefficients (slower to faster cooling)
|
||||||
|
SURFACE_COOLING_COEFFICIENTS = {
|
||||||
|
'asphalt': 0.7, # Slower cooling
|
||||||
|
'concrete': 0.85,
|
||||||
|
'mixed': 1.0, # Normal cooling
|
||||||
|
'grass': 1.5 # Faster cooling
|
||||||
|
}
|
||||||
|
|
||||||
|
# Night hours definition (7pm to 6am)
|
||||||
|
NIGHT_START_HOUR = 19 # 7pm
|
||||||
|
NIGHT_END_HOUR = 6 # 6am
|
||||||
|
|
||||||
|
# Time of day cooling multipliers
|
||||||
|
NIGHT_COOLING_MULTIPLIER = 1.3 # Night cools 30% faster
|
||||||
|
DAY_COOLING_MULTIPLIER = 1.0 # Standard cooling during day
|
@ -25,4 +25,11 @@ UV_THRESHOLD_HIGH=9
|
|||||||
# Risk assessment parameters (Optional)
|
# Risk assessment parameters (Optional)
|
||||||
RISK_THRESHOLD_SHOES=5
|
RISK_THRESHOLD_SHOES=5
|
||||||
ROLLING_WINDOW_HOURS=2
|
ROLLING_WINDOW_HOURS=2
|
||||||
SURFACE_RECOVERY_HOURS=2
|
SURFACE_RECOVERY_HOURS=2
|
||||||
|
|
||||||
|
# Surface recovery enhancements (Optional)
|
||||||
|
SURFACE_RECOVERY_TEMP_THRESHOLD=90
|
||||||
|
SURFACE_TYPE=asphalt
|
||||||
|
SURFACE_MAX_RECOVERY_SCORE=2.0
|
||||||
|
ENABLE_GRADUATED_RECOVERY=true
|
||||||
|
ENABLE_TIME_OF_DAY_FACTOR=true
|
@ -5,6 +5,8 @@ from datetime import datetime, timedelta
|
|||||||
from typing import List, Optional, Tuple
|
from typing import List, Optional, Tuple
|
||||||
from models import WeatherHour, RiskScore
|
from models import WeatherHour, RiskScore
|
||||||
from config import RiskConfig, get_config
|
from config import RiskConfig, get_config
|
||||||
|
from constants import SURFACE_COOLING_COEFFICIENTS, NIGHT_START_HOUR, NIGHT_END_HOUR
|
||||||
|
from constants import NIGHT_COOLING_MULTIPLIER, DAY_COOLING_MULTIPLIER
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -13,6 +15,21 @@ class RiskCalculator:
|
|||||||
|
|
||||||
def __init__(self, config: Optional[RiskConfig] = None):
|
def __init__(self, config: Optional[RiskConfig] = None):
|
||||||
self.config = config or get_config().risk_config
|
self.config = config or get_config().risk_config
|
||||||
|
|
||||||
|
# Validate surface type is supported
|
||||||
|
if self.config.surface_type.lower() not in SURFACE_COOLING_COEFFICIENTS:
|
||||||
|
logger.warning(f"Surface type '{self.config.surface_type}' not recognized, using 'mixed' instead")
|
||||||
|
self.config.surface_type = "mixed"
|
||||||
|
|
||||||
|
# Log enhanced recovery settings
|
||||||
|
if self.config.enable_graduated_recovery:
|
||||||
|
logger.info(f"Using graduated recovery scoring with max score of {self.config.surface_max_recovery_score}")
|
||||||
|
|
||||||
|
if self.config.enable_time_of_day_factor:
|
||||||
|
logger.info(f"Time-of-day cooling factor enabled (night: {NIGHT_COOLING_MULTIPLIER}x, day: {DAY_COOLING_MULTIPLIER}x)")
|
||||||
|
|
||||||
|
logger.info(f"Surface recovery config: threshold={self.config.surface_recovery_temp_threshold}°F, "
|
||||||
|
f"hours={self.config.surface_recovery_hours}, surface={self.config.surface_type}")
|
||||||
|
|
||||||
def calculate_temperature_score(self, temperature_f: float) -> float:
|
def calculate_temperature_score(self, temperature_f: float) -> float:
|
||||||
"""Calculate risk score based on air temperature."""
|
"""Calculate risk score based on air temperature."""
|
||||||
@ -78,25 +95,78 @@ class RiskCalculator:
|
|||||||
|
|
||||||
def calculate_surface_recovery_score(self, weather_hours: List[WeatherHour],
|
def calculate_surface_recovery_score(self, weather_hours: List[WeatherHour],
|
||||||
current_index: int) -> float:
|
current_index: int) -> float:
|
||||||
"""Calculate surface recovery score (time since last peak temperature)."""
|
"""Calculate surface recovery score with enhanced logic."""
|
||||||
if current_index < 2:
|
if current_index < 2:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
# Look back to find the last time temperature was ≥90°F
|
# Look back to find the last time temperature was above the threshold
|
||||||
hours_since_peak = 0
|
hours_since_peak = 0
|
||||||
|
sun_exposure_hours = 0
|
||||||
|
peak_temp = 0
|
||||||
|
|
||||||
|
current_hour = weather_hours[current_index].datetime
|
||||||
|
|
||||||
for i in range(current_index - 1, -1, -1):
|
for i in range(current_index - 1, -1, -1):
|
||||||
hours_since_peak += 1
|
hours_since_peak += 1
|
||||||
if weather_hours[i].temperature_f >= 90.0:
|
|
||||||
|
# Check if temperature was above recovery threshold
|
||||||
|
if weather_hours[i].temperature_f >= self.config.surface_recovery_temp_threshold:
|
||||||
|
peak_temp = weather_hours[i].temperature_f
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Count sun exposure during recovery period
|
||||||
|
condition = weather_hours[i].condition.lower()
|
||||||
|
if 'sunny' in condition or 'clear' in condition:
|
||||||
|
sun_exposure_hours += 1
|
||||||
else:
|
else:
|
||||||
# No peak found in available data
|
# No peak found in available data
|
||||||
hours_since_peak = current_index + 1
|
hours_since_peak = current_index + 1
|
||||||
|
|
||||||
# Give recovery credit if it's been >2 hours since last 90°F reading
|
# No recovery needed if no peak was found
|
||||||
if hours_since_peak > self.config.surface_recovery_hours:
|
if peak_temp == 0:
|
||||||
return -1.0
|
|
||||||
else:
|
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
|
# Apply surface type coefficient
|
||||||
|
cooling_coefficient = SURFACE_COOLING_COEFFICIENTS.get(
|
||||||
|
self.config.surface_type.lower(), 1.0)
|
||||||
|
|
||||||
|
# Apply time-of-day factor if enabled
|
||||||
|
if self.config.enable_time_of_day_factor:
|
||||||
|
hour_of_day = current_hour.hour
|
||||||
|
time_multiplier = (NIGHT_COOLING_MULTIPLIER
|
||||||
|
if hour_of_day >= NIGHT_START_HOUR or hour_of_day < NIGHT_END_HOUR
|
||||||
|
else DAY_COOLING_MULTIPLIER)
|
||||||
|
else:
|
||||||
|
time_multiplier = 1.0
|
||||||
|
|
||||||
|
# Calculate sun exposure percentage during recovery
|
||||||
|
sun_percentage = sun_exposure_hours / hours_since_peak if hours_since_peak > 0 else 0
|
||||||
|
|
||||||
|
# Sun slows cooling (reduce coefficient by up to 30%)
|
||||||
|
sun_factor = 1.0 - (sun_percentage * 0.3)
|
||||||
|
|
||||||
|
# Final adjusted hours since peak temperature
|
||||||
|
adjusted_hours = hours_since_peak * cooling_coefficient * time_multiplier * sun_factor
|
||||||
|
|
||||||
|
# Calculate recovery score
|
||||||
|
if adjusted_hours <= self.config.surface_recovery_hours:
|
||||||
|
# No recovery credit yet
|
||||||
|
return 0.0
|
||||||
|
elif self.config.enable_graduated_recovery:
|
||||||
|
# Graduated recovery (more hours = more recovery credit)
|
||||||
|
# Normalize to range 0.0 to max_recovery_score
|
||||||
|
hours_over = adjusted_hours - self.config.surface_recovery_hours
|
||||||
|
max_additional_hours = self.config.surface_recovery_hours # Full credit after double the recovery time
|
||||||
|
factor = min(1.0, hours_over / max_additional_hours)
|
||||||
|
recovery_score = -factor * self.config.surface_max_recovery_score
|
||||||
|
|
||||||
|
logger.debug(f"Graduated recovery: {hours_since_peak} hrs since {peak_temp}°F peak, "
|
||||||
|
f"adjusted to {adjusted_hours:.1f} hrs, score: {recovery_score}")
|
||||||
|
|
||||||
|
return recovery_score
|
||||||
|
else:
|
||||||
|
# Original binary approach
|
||||||
|
return -1.0
|
||||||
|
|
||||||
def interpolate_missing_uv(self, weather_hours: List[WeatherHour]) -> List[WeatherHour]:
|
def interpolate_missing_uv(self, weather_hours: List[WeatherHour]) -> List[WeatherHour]:
|
||||||
"""Interpolate missing UV values using nearby hours."""
|
"""Interpolate missing UV values using nearby hours."""
|
||||||
|
154
test_real_data.py
Normal file
154
test_real_data.py
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for enhanced surface recovery logic with real data
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from models import WeatherHour, RiskScore
|
||||||
|
from config import RiskConfig, AppConfig, get_config
|
||||||
|
from risk_calculator import RiskCalculator
|
||||||
|
from constants import SURFACE_COOLING_COEFFICIENTS
|
||||||
|
from models import DatabaseManager
|
||||||
|
|
||||||
|
# Set up logging
|
||||||
|
logging.basicConfig(level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def load_db_data():
|
||||||
|
"""Load real weather data from the database."""
|
||||||
|
config = get_config()
|
||||||
|
db_manager = DatabaseManager(config.database_path)
|
||||||
|
|
||||||
|
# Get data from the past 24 hours
|
||||||
|
end_date = datetime.now()
|
||||||
|
start_date = end_date - timedelta(hours=24)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return db_manager.get_weather_data(start_date, end_date)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error loading data from database: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def compare_recovery_settings(weather_data):
|
||||||
|
"""Compare different recovery settings with real weather data."""
|
||||||
|
if not weather_data:
|
||||||
|
logger.error("No weather data available for testing")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Loaded {len(weather_data)} hours of weather data from database")
|
||||||
|
print(f"Temperature range: {min(h.temperature_f for h in weather_data):.1f}°F - {max(h.temperature_f for h in weather_data):.1f}°F")
|
||||||
|
|
||||||
|
# Test configs
|
||||||
|
configs = [
|
||||||
|
# Default settings
|
||||||
|
RiskConfig(
|
||||||
|
surface_recovery_temp_threshold=90.0,
|
||||||
|
enable_graduated_recovery=False,
|
||||||
|
enable_time_of_day_factor=False
|
||||||
|
),
|
||||||
|
# All enhanced features
|
||||||
|
RiskConfig(
|
||||||
|
surface_recovery_temp_threshold=90.0,
|
||||||
|
enable_graduated_recovery=True,
|
||||||
|
enable_time_of_day_factor=True,
|
||||||
|
surface_max_recovery_score=2.0,
|
||||||
|
surface_type="asphalt"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Test each configuration
|
||||||
|
all_results = []
|
||||||
|
|
||||||
|
for i, config in enumerate(configs):
|
||||||
|
calculator = RiskCalculator(config)
|
||||||
|
risk_scores = calculator.calculate_risk_scores(weather_data)
|
||||||
|
all_results.append(risk_scores)
|
||||||
|
|
||||||
|
label = "Default" if i == 0 else "Enhanced"
|
||||||
|
print(f"\n=== {label} Recovery Settings ===")
|
||||||
|
|
||||||
|
high_risk = [s for s in risk_scores if s.recommend_shoes]
|
||||||
|
print(f"High Risk Hours: {len(high_risk)} out of {len(risk_scores)}")
|
||||||
|
|
||||||
|
if high_risk:
|
||||||
|
times = [s.datetime.strftime("%H:%M") for s in high_risk]
|
||||||
|
print(f"High Risk Times: {', '.join(times)}")
|
||||||
|
|
||||||
|
# Show recovery score impact
|
||||||
|
recovery_impact = sum(abs(s.surface_recovery_score) for s in risk_scores)
|
||||||
|
print(f"Total Recovery Impact: {recovery_impact:.2f}")
|
||||||
|
print(f"Average Recovery Score: {recovery_impact/len(risk_scores):.2f}")
|
||||||
|
|
||||||
|
# Visualize the results
|
||||||
|
visualize_comparison(weather_data, all_results)
|
||||||
|
|
||||||
|
def visualize_comparison(weather_data, result_sets):
|
||||||
|
"""Create visualization comparing different recovery strategies with real data."""
|
||||||
|
plt.figure(figsize=(14, 10))
|
||||||
|
|
||||||
|
# Extract time and temperatures for plotting
|
||||||
|
times = [hour.datetime for hour in weather_data]
|
||||||
|
temps = [hour.temperature_f for hour in weather_data]
|
||||||
|
|
||||||
|
# Plot temperature
|
||||||
|
ax1 = plt.subplot(3, 1, 1)
|
||||||
|
ax1.plot(times, temps, 'r-', linewidth=2)
|
||||||
|
ax1.set_ylabel('Temperature (°F)')
|
||||||
|
ax1.set_title('Temperature Profile')
|
||||||
|
ax1.axhline(y=90, color='r', linestyle='--', alpha=0.7)
|
||||||
|
ax1.text(times[0], 91, "Recovery Threshold (90°F)", color='r')
|
||||||
|
ax1.grid(True, alpha=0.3)
|
||||||
|
|
||||||
|
# Plot recovery scores
|
||||||
|
ax2 = plt.subplot(3, 1, 2, sharex=ax1)
|
||||||
|
|
||||||
|
# Add labels for legend
|
||||||
|
labels = ['Default Recovery', 'Enhanced Recovery']
|
||||||
|
linestyles = ['-', '--']
|
||||||
|
colors = ['blue', 'green']
|
||||||
|
|
||||||
|
for i, results in enumerate(result_sets):
|
||||||
|
recovery_scores = [score.surface_recovery_score for score in results]
|
||||||
|
ax2.plot(times, recovery_scores, linestyle=linestyles[i], color=colors[i], linewidth=2, label=labels[i])
|
||||||
|
|
||||||
|
ax2.set_ylabel('Recovery Score')
|
||||||
|
ax2.set_title('Surface Recovery Scores Comparison')
|
||||||
|
ax2.grid(True, alpha=0.3)
|
||||||
|
ax2.legend()
|
||||||
|
|
||||||
|
# Plot total risk scores
|
||||||
|
ax3 = plt.subplot(3, 1, 3, sharex=ax1)
|
||||||
|
|
||||||
|
for i, results in enumerate(result_sets):
|
||||||
|
total_scores = [score.total_score for score in results]
|
||||||
|
ax3.plot(times, total_scores, linestyle=linestyles[i], color=colors[i], linewidth=2, label=labels[i])
|
||||||
|
|
||||||
|
# Add threshold line
|
||||||
|
ax3.axhline(y=6.0, color='red', linestyle='--', alpha=0.7, label='Shoe Threshold (6.0)')
|
||||||
|
|
||||||
|
ax3.set_ylabel('Total Risk Score')
|
||||||
|
ax3.set_title('Total Risk Score Comparison (Real Weather Data)')
|
||||||
|
ax3.set_xlabel('Time')
|
||||||
|
ax3.grid(True, alpha=0.3)
|
||||||
|
ax3.legend()
|
||||||
|
|
||||||
|
# Format x-axis
|
||||||
|
for ax in [ax1, ax2, ax3]:
|
||||||
|
ax.set_xlim(times[0], times[-1])
|
||||||
|
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig('real_data_comparison.png')
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("Testing enhanced surface recovery logic with real data")
|
||||||
|
weather_data = load_db_data()
|
||||||
|
if weather_data:
|
||||||
|
compare_recovery_settings(weather_data)
|
||||||
|
else:
|
||||||
|
print("No data available. Please ensure you've run the app to collect weather data first.")
|
199
test_recovery.py
Normal file
199
test_recovery.py
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for enhanced surface recovery logic
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from models import WeatherHour
|
||||||
|
from config import RiskConfig
|
||||||
|
from risk_calculator import RiskCalculator
|
||||||
|
from constants import SURFACE_COOLING_COEFFICIENTS
|
||||||
|
|
||||||
|
# Set up logging
|
||||||
|
logging.basicConfig(level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def create_test_data():
|
||||||
|
"""Create synthetic test data with a temperature peak followed by cooling."""
|
||||||
|
now = datetime.now()
|
||||||
|
hours = []
|
||||||
|
|
||||||
|
# Create a 24-hour dataset
|
||||||
|
for i in range(24):
|
||||||
|
# Time starting from 24 hours ago to now
|
||||||
|
time = now - timedelta(hours=24-i)
|
||||||
|
|
||||||
|
# Temperature pattern: starts low, peaks in afternoon, drops at night
|
||||||
|
if i < 8: # Early morning (cold)
|
||||||
|
temp = 75.0 + i * 1.5
|
||||||
|
condition = "Clear"
|
||||||
|
uv = 0.0
|
||||||
|
elif i < 14: # Mid-day heating (peak at 2pm / hour 14)
|
||||||
|
temp = 85.0 + (i - 8) * 3.0
|
||||||
|
condition = "Sunny"
|
||||||
|
uv = 8.0
|
||||||
|
else: # Afternoon cooling
|
||||||
|
temp = 95.0 - (i - 14) * 2.0
|
||||||
|
condition = "Partly Cloudy" if i < 18 else "Clear"
|
||||||
|
uv = max(0.0, 8.0 - (i - 14) * 1.5)
|
||||||
|
|
||||||
|
# Create a peak temperature that exceeds threshold
|
||||||
|
if i == 13: # 1pm
|
||||||
|
temp = 95.0 # Peak temperature
|
||||||
|
|
||||||
|
hours.append(WeatherHour(
|
||||||
|
datetime=time,
|
||||||
|
temperature_f=temp,
|
||||||
|
uv_index=uv,
|
||||||
|
condition=condition,
|
||||||
|
is_forecast=False
|
||||||
|
))
|
||||||
|
|
||||||
|
return hours
|
||||||
|
|
||||||
|
def visualize_results(test_data, all_results):
|
||||||
|
"""Create visualization comparing different recovery strategies."""
|
||||||
|
plt.figure(figsize=(14, 10))
|
||||||
|
|
||||||
|
# Extract time and temperatures for plotting
|
||||||
|
times = [hour.datetime for hour in test_data]
|
||||||
|
temps = [hour.temperature_f for hour in test_data]
|
||||||
|
|
||||||
|
# Plot temperature
|
||||||
|
ax1 = plt.subplot(3, 1, 1)
|
||||||
|
ax1.plot(times, temps, 'r-', linewidth=2)
|
||||||
|
ax1.set_ylabel('Temperature (°F)')
|
||||||
|
ax1.set_title('Temperature Profile')
|
||||||
|
ax1.axhline(y=90, color='r', linestyle='--', alpha=0.7)
|
||||||
|
ax1.text(times[0], 91, "Recovery Threshold (90°F)", color='r')
|
||||||
|
ax1.grid(True, alpha=0.3)
|
||||||
|
|
||||||
|
# Plot recovery scores
|
||||||
|
ax2 = plt.subplot(3, 1, 2, sharex=ax1)
|
||||||
|
|
||||||
|
# Add labels for legend
|
||||||
|
labels = ['Default', 'Graduated', 'Time-of-day', 'All Features', 'Concrete', 'Grass']
|
||||||
|
linestyles = ['-', '--', ':', '-.', '--', ':']
|
||||||
|
colors = ['blue', 'green', 'purple', 'orange', 'brown', 'magenta']
|
||||||
|
|
||||||
|
for i, results in enumerate(all_results):
|
||||||
|
recovery_scores = [score.surface_recovery_score for score in results]
|
||||||
|
ax2.plot(times, recovery_scores, linestyle=linestyles[i], color=colors[i], linewidth=2, label=labels[i])
|
||||||
|
|
||||||
|
ax2.set_ylabel('Recovery Score')
|
||||||
|
ax2.set_title('Surface Recovery Scores Comparison')
|
||||||
|
ax2.grid(True, alpha=0.3)
|
||||||
|
ax2.legend()
|
||||||
|
|
||||||
|
# Plot total risk scores
|
||||||
|
ax3 = plt.subplot(3, 1, 3, sharex=ax1)
|
||||||
|
|
||||||
|
for i, results in enumerate(all_results):
|
||||||
|
total_scores = [score.total_score for score in results]
|
||||||
|
ax3.plot(times, total_scores, linestyle=linestyles[i], color=colors[i], linewidth=2, label=labels[i])
|
||||||
|
|
||||||
|
# Add threshold line
|
||||||
|
ax3.axhline(y=6.0, color='red', linestyle='--', alpha=0.7, label='Shoe Threshold (6.0)')
|
||||||
|
|
||||||
|
ax3.set_ylabel('Total Risk Score')
|
||||||
|
ax3.set_title('Total Risk Score Comparison')
|
||||||
|
ax3.set_xlabel('Time')
|
||||||
|
ax3.grid(True, alpha=0.3)
|
||||||
|
ax3.legend()
|
||||||
|
|
||||||
|
# Format x-axis
|
||||||
|
for ax in [ax1, ax2, ax3]:
|
||||||
|
ax.set_xlim(times[0], times[-1])
|
||||||
|
plt.setp(ax.xaxis.get_majorticklabels(), rotation=45)
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig('recovery_comparison.png')
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
def test_recovery_settings():
|
||||||
|
"""Test different recovery settings and compare results."""
|
||||||
|
test_data = create_test_data()
|
||||||
|
|
||||||
|
# Test configs
|
||||||
|
configs = [
|
||||||
|
# Default settings
|
||||||
|
RiskConfig(
|
||||||
|
surface_recovery_temp_threshold=90.0,
|
||||||
|
enable_graduated_recovery=False,
|
||||||
|
enable_time_of_day_factor=False
|
||||||
|
),
|
||||||
|
# Graduated recovery
|
||||||
|
RiskConfig(
|
||||||
|
surface_recovery_temp_threshold=90.0,
|
||||||
|
enable_graduated_recovery=True,
|
||||||
|
enable_time_of_day_factor=False,
|
||||||
|
surface_max_recovery_score=2.0
|
||||||
|
),
|
||||||
|
# Time-of-day factor
|
||||||
|
RiskConfig(
|
||||||
|
surface_recovery_temp_threshold=90.0,
|
||||||
|
enable_graduated_recovery=False,
|
||||||
|
enable_time_of_day_factor=True
|
||||||
|
),
|
||||||
|
# All features
|
||||||
|
RiskConfig(
|
||||||
|
surface_recovery_temp_threshold=90.0,
|
||||||
|
enable_graduated_recovery=True,
|
||||||
|
enable_time_of_day_factor=True,
|
||||||
|
surface_max_recovery_score=2.0
|
||||||
|
),
|
||||||
|
# Different surface types
|
||||||
|
RiskConfig(
|
||||||
|
surface_recovery_temp_threshold=90.0,
|
||||||
|
surface_type="concrete",
|
||||||
|
enable_graduated_recovery=True,
|
||||||
|
enable_time_of_day_factor=True
|
||||||
|
),
|
||||||
|
RiskConfig(
|
||||||
|
surface_recovery_temp_threshold=90.0,
|
||||||
|
surface_type="grass",
|
||||||
|
enable_graduated_recovery=True,
|
||||||
|
enable_time_of_day_factor=True
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
all_results = []
|
||||||
|
|
||||||
|
# Test each configuration
|
||||||
|
for i, config in enumerate(configs):
|
||||||
|
calculator = RiskCalculator(config)
|
||||||
|
risk_scores = calculator.calculate_risk_scores(test_data)
|
||||||
|
all_results.append(risk_scores)
|
||||||
|
|
||||||
|
print(f"\n=== Test Config {i+1} ===")
|
||||||
|
print(f"Surface Type: {config.surface_type}")
|
||||||
|
print(f"Graduated Recovery: {config.enable_graduated_recovery}")
|
||||||
|
print(f"Time-of-Day Factor: {config.enable_time_of_day_factor}")
|
||||||
|
|
||||||
|
print("\nHourly Surface Recovery Scores:")
|
||||||
|
print("Hour | Temp | Recovery Score")
|
||||||
|
print("-" * 30)
|
||||||
|
|
||||||
|
for hour, score in enumerate(risk_scores):
|
||||||
|
temp = test_data[hour].temperature_f
|
||||||
|
time = test_data[hour].datetime.strftime("%H:%M")
|
||||||
|
print(f"{time} | {temp:4.1f}°F | {score.surface_recovery_score:5.2f}")
|
||||||
|
|
||||||
|
# Show the highest risk hours
|
||||||
|
high_risk = [s for s in risk_scores if s.recommend_shoes]
|
||||||
|
print(f"\nHigh Risk Hours: {len(high_risk)} out of {len(risk_scores)}")
|
||||||
|
if high_risk:
|
||||||
|
times = [s.datetime.strftime("%H:%M") for s in high_risk]
|
||||||
|
print(f"Times: {', '.join(times)}")
|
||||||
|
|
||||||
|
# Visualize comparison
|
||||||
|
visualize_results(test_data, all_results)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("Testing enhanced surface recovery logic")
|
||||||
|
test_recovery_settings()
|
Reference in New Issue
Block a user