Untitled

 avatar
unknown
plain_text
a month ago
15 kB
3
Indexable
import json
from datetime import datetime, timedelta
from typing import Dict, List, Set
import os
import threading
import time

class MetricsHandler:
    # Environmental impact constants
    IMPACT_FACTORS = {
        'Paper': {
            'co2_per_kg': 0.9,  # kg CO2 saved per kg paper recycled
            'trees_per_kg': 0.017,  # trees saved per kg paper recycled
            'avg_item_weight': 0.1  # kg per paper item (estimated average)
        },
        'Recycling': {
            'co2_per_kg': 1.6,  # kg CO2 saved per kg plastic recycled
            'avg_item_weight': 0.05  # kg per plastic item (estimated average)
        },
        'Organic': {
            'co2_per_kg': 0.5,  # kg CO2 saved per kg organic waste composted
            'avg_item_weight': 0.2  # kg per organic waste item (estimated average)
        },
        'Residual': {
            'co2_per_kg': 0.0,  # No direct savings for residual waste
            'avg_item_weight': 0.15  # kg per residual item (estimated average)
        }
    }

    # Achievement definitions
    ACHIEVEMENTS = {
        'first_sort': {
            'id': 'first_sort',
            'name': 'First Steps',
            'description': 'Sort your first item',
            'condition': lambda metrics: metrics['basic_metrics']['total_items_sorted'] >= 1
        },
        'sorting_expert': {
            'id': 'sorting_expert',
            'tiers': ['bronze', 'silver', 'gold'],
            'name': 'Sorting Expert',
            'description': 'Sort items with high accuracy',
            'thresholds': [100, 500, 1000],  # Items sorted per tier
            'condition': lambda metrics, tier_idx: metrics['basic_metrics']['total_items_sorted'] >= ACHIEVEMENTS['sorting_expert']['thresholds'][tier_idx]
        },
        'perfect_balance': {
            'id': 'perfect_balance',
            'name': 'Perfect Balance',
            'description': 'Keep all bins within 20% fill level of each other for 7 days',
            'condition': lambda metrics: metrics.get('balance_tracking', {}).get('days_within_threshold', 0) >= 7
        },
        'time_lord': {
            'id': 'time_lord',
            'name': 'Time Lord',
            'description': 'Sort items in every hour of the day',
            'condition': lambda metrics: len(set(metrics['hour_coverage']['hours_sorted'])) == 24
        },
        'weekend_warrior': {
            'id': 'weekend_warrior',
            'name': 'Weekend Warrior',
            'description': 'Sort items on 4 consecutive weekends',
            'condition': lambda metrics: metrics['hour_coverage']['weekend_streak']['count'] >= 4
        },
        'seasonal_master': {
            'id': 'seasonal_master',
            'name': 'Seasonal Master',
            'description': 'Sort items in both summer and winter',
            'condition': lambda metrics: all(metrics['seasonal_tracking']['seasonal_counts'][season] > 0 
                                          for season in ['summer', 'winter'])
        },
        'eco_warrior': {
            'id': 'eco_warrior',
            'tiers': ['bronze', 'silver', 'gold'],
            'name': 'Eco Warrior',
            'description': 'Save CO2 through proper recycling',
            'thresholds': [10, 50, 100],  # kg CO2 saved per tier
            'condition': lambda metrics, tier_idx: metrics['environmental_impact']['co2_saved'] >= ACHIEVEMENTS['eco_warrior']['thresholds'][tier_idx]
        }
    }

    def __init__(self, metrics_file='metrics.json'):
        self.metrics_file = metrics_file
        self.metrics = self._load_metrics()

    def _load_metrics(self) -> Dict:
        """Load metrics from JSON file or create default structure"""
        if os.path.exists(self.metrics_file):
            try:
                with open(self.metrics_file, 'r') as f:
                    return json.load(f)
            except json.JSONDecodeError:
                return self._create_default_metrics()
        return self._create_default_metrics()

    def _create_default_metrics(self) -> Dict:
        """Create default metrics structure"""
        return {
            'basic_metrics': {
                'total_items_sorted': 0,
                'items_sorted_per_bin': {},
                'classification_results': [],
                'sort_timestamps': [],
                'fill_levels_per_bin': {},
                'bin_emptying_counts': {},
                'api_vs_local_usage': {'api': 0, 'local': 0}
            },
            'time_metrics': {
                'daily_usage_counts': {},
                'weekly_usage_counts': {},
                'monthly_usage_counts': {},
                'time_between_sorts': [],
                'time_of_day_patterns': [0] * 24,
                'daily_weekly_monthly_streaks': {
                    'daily': 0,
                    'weekly': 0,
                    'monthly': 0
                },
                'time_to_empty_from_90': [],
                'fill_rate_per_bin': {}
            },
            'short_term_tracking': {
                'items_last_5_minutes': {
                    'count': 0,
                    'timestamps': []
                },
                'current_sorting_streak': {
                    'start_date': None,
                    'days': 0,
                    'last_sort_date': None
                }
            },
            'hour_coverage': {
                'hours_sorted': [],
                'weekend_streak': {
                    'count': 0,
                    'dates': []
                }
            },
            'seasonal_tracking': {
                'seasonal_counts': {
                    'summer': 0,
                    'winter': 0,
                    'current_season': None,
                    'season_start': None
                }
            },
            'environmental_impact': {
                'co2_saved': 0.0,
                'trees_saved': 0.0,
                'paper_weight_recycled': 0.0,
                'plastic_weight_recycled': 0.0,
                'organic_weight_processed': 0.0
            },
            'fill_level_history': {},
            'achievements': {},
            'bin_specialization': {}
        }

    def record_sort(self, bin_id: str, classification_result: str, fill_level: float, classification_method: str):
        """Record a sorting event"""
        timestamp = datetime.now().isoformat()
        
        # Update basic metrics immediately
        self.metrics['basic_metrics']['total_items_sorted'] += 1
        self.metrics['basic_metrics']['items_sorted_per_bin'][bin_id] = \
            self.metrics['basic_metrics']['items_sorted_per_bin'].get(bin_id, 0) + 1
        self.metrics['basic_metrics']['sort_timestamps'].append(timestamp)
        self.metrics['basic_metrics']['api_vs_local_usage'][classification_method] += 1

        # Schedule delayed fill level recording
        def record_delayed_metrics():
            try:
                # Read updated fill level after delay
                with open('fill_levels.json', 'r') as f:
                    fill_levels = json.load(f)
                    current_fill_level = fill_levels.get(bin_id, 0.0)

                # Record sort details
                sort_record = {
                    'timestamp': timestamp,
                    'bin_id': bin_id,
                    'classification_result': classification_result,
                    'fill_level': current_fill_level,
                    'method': classification_method
                }
                self.metrics['basic_metrics']['classification_results'].append(sort_record)
                self.metrics['basic_metrics']['fill_levels_per_bin'][bin_id] = current_fill_level

                # Update environmental impact
                self._update_environmental_impact(classification_result)
                
                # Update time-based metrics
                self._update_time_metrics(timestamp)
                
                # Update seasonal tracking
                self._update_seasonal_tracking(timestamp)
                
                # Update achievements
                self._check_achievements()
                
                # Save all updates
                self.save_metrics()

            except Exception as e:
                print(f"Error recording delayed metrics: {e}")

        # Start delayed recording thread
        thread = threading.Thread(target=lambda: [time.sleep(6), record_delayed_metrics()])
        thread.daemon = True
        thread.start()

    def _update_environmental_impact(self, classification_result: str):
        """Update environmental impact calculations"""
        impact_factor = self.IMPACT_FACTORS.get(classification_result)
        if impact_factor:
            # Calculate environmental impact
            item_weight = impact_factor['avg_item_weight']
            
            if classification_result == 'Paper':
                self.metrics['environmental_impact']['paper_weight_recycled'] += item_weight
                self.metrics['environmental_impact']['trees_saved'] += item_weight * impact_factor['trees_per_kg']
                
            elif classification_result == 'Recycling':
                self.metrics['environmental_impact']['plastic_weight_recycled'] += item_weight
                
            elif classification_result == 'Organic':
                self.metrics['environmental_impact']['organic_weight_processed'] += item_weight
                
            # Calculate CO2 savings
            co2_saved = item_weight * impact_factor['co2_per_kg']
            self.metrics['environmental_impact']['co2_saved'] += co2_saved

    def _update_time_metrics(self, timestamp: str):
        """Update time-based metrics"""
        current_time = datetime.fromisoformat(timestamp)
        
        # Update daily/weekly/monthly counts
        date_str = current_time.date().isoformat()
        week_str = f"{current_time.year}-W{current_time.isocalendar()[1]}"
        month_str = f"{current_time.year}-{current_time.month:02d}"
        
        self.metrics['time_metrics']['daily_usage_counts'][date_str] = \
            self.metrics['time_metrics']['daily_usage_counts'].get(date_str, 0) + 1
        
        self.metrics['time_metrics']['weekly_usage_counts'][week_str] = \
            self.metrics['time_metrics']['weekly_usage_counts'].get(week_str, 0) + 1
            
        self.metrics['time_metrics']['monthly_usage_counts'][month_str] = \
            self.metrics['time_metrics']['monthly_usage_counts'].get(month_str, 0) + 1

        # Update time of day patterns
        hour = current_time.hour
        self.metrics['time_metrics']['time_of_day_patterns'][hour] += 1

        # Update hour coverage
        if hour not in self.metrics['hour_coverage']['hours_sorted']:
            self.metrics['hour_coverage']['hours_sorted'].append(hour)

        # Update weekend tracking
        if current_time.weekday() >= 5:  # Saturday = 5, Sunday = 6
            weekend_dates = self.metrics['hour_coverage']['weekend_streak']['dates']
            if not weekend_dates or \
               (current_time.date() - datetime.fromisoformat(weekend_dates[-1]).date()).days > 1:
                weekend_dates.append(timestamp)
                
                # Check for consecutive weekends
                if len(weekend_dates) >= 2:
                    date1 = datetime.fromisoformat(weekend_dates[-2]).date()
                    date2 = current_time.date()
                    if (date2 - date1).days <= 8:  # Within 8 days = consecutive weekends
                        self.metrics['hour_coverage']['weekend_streak']['count'] += 1
                    else:
                        self.metrics['hour_coverage']['weekend_streak']['count'] = 1

    def _update_seasonal_tracking(self, timestamp: str):
        """Update seasonal tracking"""
        current_time = datetime.fromisoformat(timestamp)
        month = current_time.month
        
        # Simple season determination (Northern Hemisphere)
        current_season = 'summer' if 5 <= month <= 8 else 'winter'
        
        # Update season if changed
        if self.metrics['seasonal_tracking']['current_season'] != current_season:
            self.metrics['seasonal_tracking']['current_season'] = current_season
            self.metrics['seasonal_tracking']['season_start'] = timestamp
            
        # Increment seasonal count
        self.metrics['seasonal_tracking']['seasonal_counts'][current_season] += 1

    def _check_achievements(self):
        """Check and update achievements"""
        if 'achievements' not in self.metrics:
            self.metrics['achievements'] = {}
            
        # Check each achievement
        for achievement_id, achievement in self.ACHIEVEMENTS.items():
            if 'tiers' in achievement:
                # Multi-tier achievement
                for tier_idx, tier in enumerate(achievement['tiers']):
                    achievement_key = f"{achievement_id}_{tier}"
                    if achievement_key not in self.metrics['achievements']:
                        if achievement['condition'](self.metrics, tier_idx):
                            self.metrics['achievements'][achievement_key] = {
                                'status': 'completed',
                                'unlock_date': datetime.now().isoformat(),
                                'tier': tier
                            }
            else:
                # Single-tier achievement
                if achievement_id not in self.metrics['achievements']:
                    if achievement['condition'](self.metrics):
                        self.metrics['achievements'][achievement_id] = {
                            'status': 'completed',
                            'unlock_date': datetime.now().isoformat()
                        }

    def check_bin_balance(self):
        """Check if bins are balanced (within 20% of each other)"""
        fill_levels = [level for level in self.metrics['basic_metrics']['fill_levels_per_bin'].values()]
        if not fill_levels:
            return False
            
        max_level = max(fill_levels)
        min_level = min(fill_levels)
        return (max_level - min_level) <= 20.0

    def save_metrics(self):
        """Save metrics to JSON file"""
        with open(self.metrics_file, 'w') as f:
            json.dump(self.metrics, f, indent=2)

    def get_metrics(self) -> Dict:
        """Get all metrics"""
        return self.metrics

    def get_metric(self, category: str, metric: str) -> any:
        """Get a specific metric"""
        return self.metrics.get(category, {}).get(metric, None)
Editor is loading...
Leave a Comment