mirror of
https://github.com/sstent/Garmin_Analyser.git
synced 2025-12-06 08:01:40 +00:00
sync
This commit is contained in:
16
models/__init__.py
Normal file
16
models/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Data models for Garmin Analyser."""
|
||||
|
||||
from .workout import WorkoutData, WorkoutMetadata, PowerData, HeartRateData, SpeedData, ElevationData, GearData
|
||||
from .zones import ZoneDefinition, ZoneCalculator
|
||||
|
||||
__all__ = [
|
||||
'WorkoutData',
|
||||
'WorkoutMetadata',
|
||||
'PowerData',
|
||||
'HeartRateData',
|
||||
'SpeedData',
|
||||
'ElevationData',
|
||||
'GearData',
|
||||
'ZoneDefinition',
|
||||
'ZoneCalculator'
|
||||
]
|
||||
134
models/workout.py
Normal file
134
models/workout.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""Data models for workout analysis."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
import pandas as pd
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkoutMetadata:
|
||||
"""Metadata for a workout session."""
|
||||
|
||||
activity_id: str
|
||||
activity_name: str
|
||||
start_time: datetime
|
||||
duration_seconds: float
|
||||
distance_meters: Optional[float] = None
|
||||
avg_heart_rate: Optional[float] = None
|
||||
max_heart_rate: Optional[float] = None
|
||||
avg_power: Optional[float] = None
|
||||
max_power: Optional[float] = None
|
||||
avg_speed: Optional[float] = None
|
||||
max_speed: Optional[float] = None
|
||||
elevation_gain: Optional[float] = None
|
||||
elevation_loss: Optional[float] = None
|
||||
calories: Optional[float] = None
|
||||
sport: str = "cycling"
|
||||
sub_sport: Optional[str] = None
|
||||
is_indoor: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class PowerData:
|
||||
"""Power-related data for a workout."""
|
||||
|
||||
power_values: List[float]
|
||||
estimated_power: List[float]
|
||||
power_zones: Dict[str, int]
|
||||
normalized_power: Optional[float] = None
|
||||
intensity_factor: Optional[float] = None
|
||||
training_stress_score: Optional[float] = None
|
||||
power_distribution: Dict[str, float] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class HeartRateData:
|
||||
"""Heart rate data for a workout."""
|
||||
|
||||
heart_rate_values: List[float]
|
||||
hr_zones: Dict[str, int]
|
||||
avg_hr: Optional[float] = None
|
||||
max_hr: Optional[float] = None
|
||||
hr_distribution: Dict[str, float] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpeedData:
|
||||
"""Speed and distance data for a workout."""
|
||||
|
||||
speed_values: List[float]
|
||||
distance_values: List[float]
|
||||
avg_speed: Optional[float] = None
|
||||
max_speed: Optional[float] = None
|
||||
total_distance: Optional[float] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ElevationData:
|
||||
"""Elevation and gradient data for a workout."""
|
||||
|
||||
elevation_values: List[float]
|
||||
gradient_values: List[float]
|
||||
elevation_gain: Optional[float] = None
|
||||
elevation_loss: Optional[float] = None
|
||||
max_gradient: Optional[float] = None
|
||||
min_gradient: Optional[float] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class GearData:
|
||||
"""Gear-related data for a workout."""
|
||||
|
||||
gear_ratios: List[float]
|
||||
cadence_values: List[float]
|
||||
estimated_gear: List[str]
|
||||
chainring_teeth: int
|
||||
cassette_teeth: List[int]
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkoutData:
|
||||
"""Complete workout data structure."""
|
||||
|
||||
metadata: WorkoutMetadata
|
||||
power: Optional[PowerData] = None
|
||||
heart_rate: Optional[HeartRateData] = None
|
||||
speed: Optional[SpeedData] = None
|
||||
elevation: Optional[ElevationData] = None
|
||||
gear: Optional[GearData] = None
|
||||
raw_data: Optional[pd.DataFrame] = None
|
||||
|
||||
@property
|
||||
def has_power_data(self) -> bool:
|
||||
"""Check if actual power data is available."""
|
||||
return self.power is not None and any(p > 0 for p in self.power.power_values)
|
||||
|
||||
@property
|
||||
def duration_minutes(self) -> float:
|
||||
"""Get duration in minutes."""
|
||||
return self.metadata.duration_seconds / 60
|
||||
|
||||
@property
|
||||
def distance_km(self) -> Optional[float]:
|
||||
"""Get distance in kilometers."""
|
||||
if self.metadata.distance_meters is None:
|
||||
return None
|
||||
return self.metadata.distance_meters / 1000
|
||||
|
||||
def get_summary(self) -> Dict[str, Any]:
|
||||
"""Get a summary of the workout."""
|
||||
return {
|
||||
"activity_id": self.metadata.activity_id,
|
||||
"activity_name": self.metadata.activity_name,
|
||||
"start_time": self.metadata.start_time.isoformat(),
|
||||
"duration_minutes": round(self.duration_minutes, 1),
|
||||
"distance_km": round(self.distance_km, 2) if self.distance_km else None,
|
||||
"avg_heart_rate": self.metadata.avg_heart_rate,
|
||||
"max_heart_rate": self.metadata.max_heart_rate,
|
||||
"avg_power": self.metadata.avg_power,
|
||||
"max_power": self.metadata.max_power,
|
||||
"elevation_gain": self.metadata.elevation_gain,
|
||||
"is_indoor": self.metadata.is_indoor,
|
||||
"has_power_data": self.has_power_data
|
||||
}
|
||||
193
models/zones.py
Normal file
193
models/zones.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""Zone definitions and calculations for workouts."""
|
||||
|
||||
from typing import Dict, Tuple, List
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ZoneDefinition:
|
||||
"""Definition of a training zone."""
|
||||
|
||||
name: str
|
||||
min_value: float
|
||||
max_value: float
|
||||
color: str
|
||||
description: str
|
||||
|
||||
|
||||
class ZoneCalculator:
|
||||
"""Calculator for various training zones."""
|
||||
|
||||
@staticmethod
|
||||
def get_power_zones() -> Dict[str, ZoneDefinition]:
|
||||
"""Get power zone definitions."""
|
||||
return {
|
||||
'Recovery': ZoneDefinition(
|
||||
name='Recovery',
|
||||
min_value=0,
|
||||
max_value=150,
|
||||
color='lightblue',
|
||||
description='Active recovery, very light effort'
|
||||
),
|
||||
'Endurance': ZoneDefinition(
|
||||
name='Endurance',
|
||||
min_value=150,
|
||||
max_value=200,
|
||||
color='green',
|
||||
description='Aerobic base, sustainable for hours'
|
||||
),
|
||||
'Tempo': ZoneDefinition(
|
||||
name='Tempo',
|
||||
min_value=200,
|
||||
max_value=250,
|
||||
color='yellow',
|
||||
description='Sweet spot, sustainable for 20-60 minutes'
|
||||
),
|
||||
'Threshold': ZoneDefinition(
|
||||
name='Threshold',
|
||||
min_value=250,
|
||||
max_value=300,
|
||||
color='orange',
|
||||
description='Functional threshold power, 20-60 minutes'
|
||||
),
|
||||
'VO2 Max': ZoneDefinition(
|
||||
name='VO2 Max',
|
||||
min_value=300,
|
||||
max_value=1000,
|
||||
color='red',
|
||||
description='Maximum aerobic capacity, 3-8 minutes'
|
||||
)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_heart_rate_zones(lthr: int = 170) -> Dict[str, ZoneDefinition]:
|
||||
"""Get heart rate zone definitions based on lactate threshold.
|
||||
|
||||
Args:
|
||||
lthr: Lactate threshold heart rate in bpm
|
||||
|
||||
Returns:
|
||||
Dictionary of heart rate zones
|
||||
"""
|
||||
return {
|
||||
'Z1': ZoneDefinition(
|
||||
name='Zone 1',
|
||||
min_value=0,
|
||||
max_value=int(lthr * 0.8),
|
||||
color='lightblue',
|
||||
description='Active recovery, <80% LTHR'
|
||||
),
|
||||
'Z2': ZoneDefinition(
|
||||
name='Zone 2',
|
||||
min_value=int(lthr * 0.8),
|
||||
max_value=int(lthr * 0.87),
|
||||
color='green',
|
||||
description='Aerobic base, 80-87% LTHR'
|
||||
),
|
||||
'Z3': ZoneDefinition(
|
||||
name='Zone 3',
|
||||
min_value=int(lthr * 0.87) + 1,
|
||||
max_value=int(lthr * 0.93),
|
||||
color='yellow',
|
||||
description='Tempo, 88-93% LTHR'
|
||||
),
|
||||
'Z4': ZoneDefinition(
|
||||
name='Zone 4',
|
||||
min_value=int(lthr * 0.93) + 1,
|
||||
max_value=int(lthr * 0.99),
|
||||
color='orange',
|
||||
description='Threshold, 94-99% LTHR'
|
||||
),
|
||||
'Z5': ZoneDefinition(
|
||||
name='Zone 5',
|
||||
min_value=int(lthr * 0.99) + 1,
|
||||
max_value=300,
|
||||
color='red',
|
||||
description='VO2 Max, >99% LTHR'
|
||||
)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def calculate_zone_distribution(values: List[float], zones: Dict[str, ZoneDefinition]) -> Dict[str, float]:
|
||||
"""Calculate time spent in each zone.
|
||||
|
||||
Args:
|
||||
values: List of values (power, heart rate, etc.)
|
||||
zones: Zone definitions
|
||||
|
||||
Returns:
|
||||
Dictionary with percentage time in each zone
|
||||
"""
|
||||
if not values:
|
||||
return {zone_name: 0.0 for zone_name in zones.keys()}
|
||||
|
||||
zone_counts = {zone_name: 0 for zone_name in zones.keys()}
|
||||
|
||||
for value in values:
|
||||
for zone_name, zone_def in zones.items():
|
||||
if zone_def.min_value <= value <= zone_def.max_value:
|
||||
zone_counts[zone_name] += 1
|
||||
break
|
||||
|
||||
total_count = len(values)
|
||||
return {
|
||||
zone_name: (count / total_count) * 100
|
||||
for zone_name, count in zone_counts.items()
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_zone_for_value(value: float, zones: Dict[str, ZoneDefinition]) -> str:
|
||||
"""Get the zone name for a given value.
|
||||
|
||||
Args:
|
||||
value: The value to check
|
||||
zones: Zone definitions
|
||||
|
||||
Returns:
|
||||
Zone name or 'Unknown' if not found
|
||||
"""
|
||||
for zone_name, zone_def in zones.items():
|
||||
if zone_def.min_value <= value <= zone_def.max_value:
|
||||
return zone_name
|
||||
return 'Unknown'
|
||||
|
||||
@staticmethod
|
||||
def get_cadence_zones() -> Dict[str, ZoneDefinition]:
|
||||
"""Get cadence zone definitions."""
|
||||
return {
|
||||
'Recovery': ZoneDefinition(
|
||||
name='Recovery',
|
||||
min_value=0,
|
||||
max_value=80,
|
||||
color='lightblue',
|
||||
description='Low cadence, recovery pace'
|
||||
),
|
||||
'Endurance': ZoneDefinition(
|
||||
name='Endurance',
|
||||
min_value=80,
|
||||
max_value=90,
|
||||
color='green',
|
||||
description='Comfortable cadence, sustainable'
|
||||
),
|
||||
'Tempo': ZoneDefinition(
|
||||
name='Tempo',
|
||||
min_value=90,
|
||||
max_value=100,
|
||||
color='yellow',
|
||||
description='Moderate cadence, tempo effort'
|
||||
),
|
||||
'Threshold': ZoneDefinition(
|
||||
name='Threshold',
|
||||
min_value=100,
|
||||
max_value=110,
|
||||
color='orange',
|
||||
description='High cadence, threshold effort'
|
||||
),
|
||||
'Sprint': ZoneDefinition(
|
||||
name='Sprint',
|
||||
min_value=110,
|
||||
max_value=200,
|
||||
color='red',
|
||||
description='Maximum cadence, sprint effort'
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user