Spaces:
Sleeping
Sleeping
Delete unified_data_manager.py
Browse files- unified_data_manager.py +0 -413
unified_data_manager.py
DELETED
|
@@ -1,413 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Unified Data Manager for GlycoAI - FIXED VERSION
|
| 3 |
-
Restores the original working API calls that were working before
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import logging
|
| 7 |
-
from typing import Dict, Any, Optional, Tuple
|
| 8 |
-
import pandas as pd
|
| 9 |
-
from datetime import datetime, timedelta
|
| 10 |
-
from dataclasses import asdict
|
| 11 |
-
|
| 12 |
-
from apifunctions import (
|
| 13 |
-
DexcomAPI,
|
| 14 |
-
GlucoseAnalyzer,
|
| 15 |
-
DEMO_USERS,
|
| 16 |
-
DemoUser
|
| 17 |
-
)
|
| 18 |
-
|
| 19 |
-
logger = logging.getLogger(__name__)
|
| 20 |
-
|
| 21 |
-
class UnifiedDataManager:
|
| 22 |
-
"""
|
| 23 |
-
FIXED: Unified data manager that calls the API exactly as it was working before
|
| 24 |
-
"""
|
| 25 |
-
|
| 26 |
-
def __init__(self):
|
| 27 |
-
self.dexcom_api = DexcomAPI()
|
| 28 |
-
self.analyzer = GlucoseAnalyzer()
|
| 29 |
-
|
| 30 |
-
logger.info(f"UnifiedDataManager initialized - RESTORED to working version")
|
| 31 |
-
|
| 32 |
-
# Single source of truth for all data
|
| 33 |
-
self.current_user: Optional[DemoUser] = None
|
| 34 |
-
self.raw_glucose_data: Optional[list] = None
|
| 35 |
-
self.processed_glucose_data: Optional[pd.DataFrame] = None
|
| 36 |
-
self.calculated_stats: Optional[Dict] = None
|
| 37 |
-
self.identified_patterns: Optional[Dict] = None
|
| 38 |
-
|
| 39 |
-
# Metadata
|
| 40 |
-
self.data_loaded_at: Optional[datetime] = None
|
| 41 |
-
self.data_source: str = "none" # "dexcom_api", "mock", or "none"
|
| 42 |
-
|
| 43 |
-
def load_user_data(self, user_key: str, force_reload: bool = False) -> Dict[str, Any]:
|
| 44 |
-
"""
|
| 45 |
-
FIXED: Load glucose data using the ORIGINAL WORKING method
|
| 46 |
-
"""
|
| 47 |
-
|
| 48 |
-
# Check if we already have data for this user and it's recent
|
| 49 |
-
if (not force_reload and
|
| 50 |
-
self.current_user and
|
| 51 |
-
self.current_user == DEMO_USERS.get(user_key) and
|
| 52 |
-
self.data_loaded_at and
|
| 53 |
-
(datetime.now() - self.data_loaded_at).seconds < 300): # 5 minutes cache
|
| 54 |
-
|
| 55 |
-
logger.info(f"Using cached data for {user_key}")
|
| 56 |
-
return self._build_success_response()
|
| 57 |
-
|
| 58 |
-
try:
|
| 59 |
-
if user_key not in DEMO_USERS:
|
| 60 |
-
return {
|
| 61 |
-
"success": False,
|
| 62 |
-
"message": f"❌ Invalid user key '{user_key}'. Available: {', '.join(DEMO_USERS.keys())}"
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
logger.info(f"Loading data for user: {user_key}")
|
| 66 |
-
|
| 67 |
-
# Set current user
|
| 68 |
-
self.current_user = DEMO_USERS[user_key]
|
| 69 |
-
|
| 70 |
-
# Call API EXACTLY as it was working before
|
| 71 |
-
try:
|
| 72 |
-
logger.info(f"Attempting Dexcom API authentication for {user_key}")
|
| 73 |
-
|
| 74 |
-
# ORIGINAL WORKING METHOD: Use the simulate_demo_login exactly as before
|
| 75 |
-
access_token = self.dexcom_api.simulate_demo_login(user_key)
|
| 76 |
-
logger.info(f"Dexcom authentication result: {bool(access_token)}")
|
| 77 |
-
|
| 78 |
-
if access_token:
|
| 79 |
-
# ORIGINAL WORKING METHOD: Get data with 14-day range
|
| 80 |
-
end_date = datetime.now()
|
| 81 |
-
start_date = end_date - timedelta(days=14)
|
| 82 |
-
|
| 83 |
-
# Call get_egv_data EXACTLY as it was working before
|
| 84 |
-
self.raw_glucose_data = self.dexcom_api.get_egv_data(
|
| 85 |
-
start_date.isoformat(),
|
| 86 |
-
end_date.isoformat()
|
| 87 |
-
)
|
| 88 |
-
|
| 89 |
-
if self.raw_glucose_data and len(self.raw_glucose_data) > 0:
|
| 90 |
-
self.data_source = "dexcom_api"
|
| 91 |
-
logger.info(f"✅ Successfully loaded {len(self.raw_glucose_data)} readings from Dexcom API")
|
| 92 |
-
else:
|
| 93 |
-
logger.warning("Dexcom API returned empty data - falling back to mock data")
|
| 94 |
-
raise Exception("Empty data from Dexcom API")
|
| 95 |
-
else:
|
| 96 |
-
logger.warning("Failed to get access token - falling back to mock data")
|
| 97 |
-
raise Exception("Authentication failed")
|
| 98 |
-
|
| 99 |
-
except Exception as api_error:
|
| 100 |
-
logger.warning(f"Dexcom API failed ({str(api_error)}) - using mock data fallback")
|
| 101 |
-
self.raw_glucose_data = self._generate_realistic_mock_data(user_key)
|
| 102 |
-
self.data_source = "mock"
|
| 103 |
-
|
| 104 |
-
# Process the raw data (same processing for everyone)
|
| 105 |
-
self.processed_glucose_data = self.analyzer.process_egv_data(self.raw_glucose_data)
|
| 106 |
-
|
| 107 |
-
if self.processed_glucose_data is None or self.processed_glucose_data.empty:
|
| 108 |
-
return {
|
| 109 |
-
"success": False,
|
| 110 |
-
"message": "❌ Failed to process glucose data"
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
# Calculate statistics (single source of truth)
|
| 114 |
-
self.calculated_stats = self._calculate_unified_stats()
|
| 115 |
-
|
| 116 |
-
# Identify patterns
|
| 117 |
-
self.identified_patterns = self.analyzer.identify_patterns(self.processed_glucose_data)
|
| 118 |
-
|
| 119 |
-
# Mark when data was loaded
|
| 120 |
-
self.data_loaded_at = datetime.now()
|
| 121 |
-
|
| 122 |
-
logger.info(f"Successfully loaded and processed data for {self.current_user.name}")
|
| 123 |
-
logger.info(f"Data source: {self.data_source}, Readings: {len(self.processed_glucose_data)}")
|
| 124 |
-
logger.info(f"TIR: {self.calculated_stats.get('time_in_range_70_180', 0):.1f}%")
|
| 125 |
-
|
| 126 |
-
return self._build_success_response()
|
| 127 |
-
|
| 128 |
-
except Exception as e:
|
| 129 |
-
logger.error(f"Failed to load user data: {e}")
|
| 130 |
-
return {
|
| 131 |
-
"success": False,
|
| 132 |
-
"message": f"❌ Failed to load user data: {str(e)}"
|
| 133 |
-
}
|
| 134 |
-
|
| 135 |
-
def get_stats_for_ui(self) -> Dict[str, Any]:
|
| 136 |
-
"""Get statistics formatted for the UI display"""
|
| 137 |
-
if not self.calculated_stats:
|
| 138 |
-
return {}
|
| 139 |
-
|
| 140 |
-
return {
|
| 141 |
-
**self.calculated_stats,
|
| 142 |
-
"data_source": self.data_source,
|
| 143 |
-
"loaded_at": self.data_loaded_at.isoformat() if self.data_loaded_at else None,
|
| 144 |
-
"user_name": self.current_user.name if self.current_user else None
|
| 145 |
-
}
|
| 146 |
-
|
| 147 |
-
def get_context_for_agent(self) -> Dict[str, Any]:
|
| 148 |
-
"""Get context formatted for the AI agent"""
|
| 149 |
-
if not self.current_user or not self.calculated_stats:
|
| 150 |
-
return {"error": "No user data loaded"}
|
| 151 |
-
|
| 152 |
-
# Build agent context with the SAME data as UI
|
| 153 |
-
context = {
|
| 154 |
-
"user": {
|
| 155 |
-
"name": self.current_user.name,
|
| 156 |
-
"age": self.current_user.age,
|
| 157 |
-
"diabetes_type": self.current_user.diabetes_type,
|
| 158 |
-
"device_type": self.current_user.device_type,
|
| 159 |
-
"years_with_diabetes": self.current_user.years_with_diabetes,
|
| 160 |
-
"typical_pattern": getattr(self.current_user, 'typical_glucose_pattern', 'normal')
|
| 161 |
-
},
|
| 162 |
-
"statistics": self._safe_convert_for_json(self.calculated_stats),
|
| 163 |
-
"patterns": self._safe_convert_for_json(self.identified_patterns),
|
| 164 |
-
"data_points": len(self.processed_glucose_data) if self.processed_glucose_data is not None else 0,
|
| 165 |
-
"recent_readings": self._get_recent_readings_for_agent(),
|
| 166 |
-
"data_metadata": {
|
| 167 |
-
"source": self.data_source,
|
| 168 |
-
"loaded_at": self.data_loaded_at.isoformat() if self.data_loaded_at else None,
|
| 169 |
-
"data_age_minutes": int((datetime.now() - self.data_loaded_at).total_seconds() / 60) if self.data_loaded_at else None
|
| 170 |
-
}
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
return context
|
| 174 |
-
|
| 175 |
-
def get_chart_data(self) -> Optional[pd.DataFrame]:
|
| 176 |
-
"""Get processed data for chart display"""
|
| 177 |
-
return self.processed_glucose_data
|
| 178 |
-
|
| 179 |
-
def _calculate_unified_stats(self) -> Dict[str, Any]:
|
| 180 |
-
"""Calculate statistics using a single, consistent method"""
|
| 181 |
-
if self.processed_glucose_data is None or self.processed_glucose_data.empty:
|
| 182 |
-
return {"error": "No data available"}
|
| 183 |
-
|
| 184 |
-
try:
|
| 185 |
-
# Get glucose values
|
| 186 |
-
glucose_values = self.processed_glucose_data['value'].dropna()
|
| 187 |
-
|
| 188 |
-
if len(glucose_values) == 0:
|
| 189 |
-
return {"error": "No valid glucose values"}
|
| 190 |
-
|
| 191 |
-
# Convert to numpy array for consistent calculations
|
| 192 |
-
import numpy as np
|
| 193 |
-
values = np.array(glucose_values.tolist(), dtype=float)
|
| 194 |
-
|
| 195 |
-
# Calculate basic statistics
|
| 196 |
-
avg_glucose = float(np.mean(values))
|
| 197 |
-
min_glucose = float(np.min(values))
|
| 198 |
-
max_glucose = float(np.max(values))
|
| 199 |
-
std_glucose = float(np.std(values))
|
| 200 |
-
total_readings = int(len(values))
|
| 201 |
-
|
| 202 |
-
# Calculate time in ranges - CONSISTENT METHOD
|
| 203 |
-
in_range_mask = (values >= 70) & (values <= 180)
|
| 204 |
-
below_range_mask = values < 70
|
| 205 |
-
above_range_mask = values > 180
|
| 206 |
-
|
| 207 |
-
in_range_count = int(np.sum(in_range_mask))
|
| 208 |
-
below_range_count = int(np.sum(below_range_mask))
|
| 209 |
-
above_range_count = int(np.sum(above_range_mask))
|
| 210 |
-
|
| 211 |
-
# Calculate percentages
|
| 212 |
-
time_in_range = (in_range_count / total_readings) * 100 if total_readings > 0 else 0
|
| 213 |
-
time_below_70 = (below_range_count / total_readings) * 100 if total_readings > 0 else 0
|
| 214 |
-
time_above_180 = (above_range_count / total_readings) * 100 if total_readings > 0 else 0
|
| 215 |
-
|
| 216 |
-
# Calculate additional metrics
|
| 217 |
-
gmi = 3.31 + (0.02392 * avg_glucose) # Glucose Management Indicator
|
| 218 |
-
cv = (std_glucose / avg_glucose) * 100 if avg_glucose > 0 else 0 # Coefficient of Variation
|
| 219 |
-
|
| 220 |
-
stats = {
|
| 221 |
-
"average_glucose": avg_glucose,
|
| 222 |
-
"min_glucose": min_glucose,
|
| 223 |
-
"max_glucose": max_glucose,
|
| 224 |
-
"std_glucose": std_glucose,
|
| 225 |
-
"time_in_range_70_180": time_in_range,
|
| 226 |
-
"time_below_70": time_below_70,
|
| 227 |
-
"time_above_180": time_above_180,
|
| 228 |
-
"total_readings": total_readings,
|
| 229 |
-
"gmi": gmi,
|
| 230 |
-
"cv": cv,
|
| 231 |
-
"in_range_count": in_range_count,
|
| 232 |
-
"below_range_count": below_range_count,
|
| 233 |
-
"above_range_count": above_range_count
|
| 234 |
-
}
|
| 235 |
-
|
| 236 |
-
# Log for debugging
|
| 237 |
-
logger.info(f"Calculated stats - TIR: {time_in_range:.1f}%, Total: {total_readings}, In range: {in_range_count}")
|
| 238 |
-
|
| 239 |
-
return stats
|
| 240 |
-
|
| 241 |
-
except Exception as e:
|
| 242 |
-
logger.error(f"Error calculating unified stats: {e}")
|
| 243 |
-
return {"error": f"Statistics calculation failed: {str(e)}"}
|
| 244 |
-
|
| 245 |
-
def _generate_realistic_mock_data(self, user_key: str) -> list:
|
| 246 |
-
"""Generate consistent mock data for demo users"""
|
| 247 |
-
from mistral_chat import GlucoseDataGenerator
|
| 248 |
-
|
| 249 |
-
# Map users to patterns
|
| 250 |
-
pattern_map = {
|
| 251 |
-
"sarah_g7": "normal",
|
| 252 |
-
"marcus_one": "dawn_phenomenon",
|
| 253 |
-
"jennifer_g6": "normal",
|
| 254 |
-
"robert_receiver": "dawn_phenomenon"
|
| 255 |
-
}
|
| 256 |
-
|
| 257 |
-
user_pattern = pattern_map.get(user_key, "normal")
|
| 258 |
-
|
| 259 |
-
# Generate 14 days of data
|
| 260 |
-
mock_data = GlucoseDataGenerator.create_realistic_pattern(days=14, user_type=user_pattern)
|
| 261 |
-
|
| 262 |
-
logger.info(f"Generated {len(mock_data)} mock data points for {user_key} with pattern {user_pattern}")
|
| 263 |
-
|
| 264 |
-
return mock_data
|
| 265 |
-
|
| 266 |
-
def _get_recent_readings_for_agent(self, count: int = 5) -> list:
|
| 267 |
-
"""Get recent readings formatted for agent context"""
|
| 268 |
-
if self.processed_glucose_data is None or self.processed_glucose_data.empty:
|
| 269 |
-
return []
|
| 270 |
-
|
| 271 |
-
try:
|
| 272 |
-
recent_df = self.processed_glucose_data.tail(count)
|
| 273 |
-
readings = []
|
| 274 |
-
|
| 275 |
-
for _, row in recent_df.iterrows():
|
| 276 |
-
display_time = row.get('displayTime') or row.get('systemTime')
|
| 277 |
-
glucose_value = row.get('value')
|
| 278 |
-
trend_value = row.get('trend', 'flat')
|
| 279 |
-
|
| 280 |
-
if pd.notna(display_time):
|
| 281 |
-
if isinstance(display_time, str):
|
| 282 |
-
time_str = display_time
|
| 283 |
-
else:
|
| 284 |
-
time_str = pd.to_datetime(display_time).isoformat()
|
| 285 |
-
else:
|
| 286 |
-
time_str = datetime.now().isoformat()
|
| 287 |
-
|
| 288 |
-
if pd.notna(glucose_value):
|
| 289 |
-
glucose_clean = self._safe_convert_for_json(glucose_value)
|
| 290 |
-
else:
|
| 291 |
-
glucose_clean = None
|
| 292 |
-
|
| 293 |
-
trend_clean = str(trend_value) if pd.notna(trend_value) else 'flat'
|
| 294 |
-
|
| 295 |
-
readings.append({
|
| 296 |
-
"time": time_str,
|
| 297 |
-
"glucose": glucose_clean,
|
| 298 |
-
"trend": trend_clean
|
| 299 |
-
})
|
| 300 |
-
|
| 301 |
-
return readings
|
| 302 |
-
|
| 303 |
-
except Exception as e:
|
| 304 |
-
logger.error(f"Error getting recent readings: {e}")
|
| 305 |
-
return []
|
| 306 |
-
|
| 307 |
-
def _safe_convert_for_json(self, obj):
|
| 308 |
-
"""Safely convert objects for JSON serialization"""
|
| 309 |
-
import numpy as np
|
| 310 |
-
|
| 311 |
-
if obj is None:
|
| 312 |
-
return None
|
| 313 |
-
elif isinstance(obj, (np.integer, np.int64, np.int32)):
|
| 314 |
-
return int(obj)
|
| 315 |
-
elif isinstance(obj, (np.floating, np.float64, np.float32)):
|
| 316 |
-
if np.isnan(obj):
|
| 317 |
-
return None
|
| 318 |
-
return float(obj)
|
| 319 |
-
elif isinstance(obj, dict):
|
| 320 |
-
return {key: self._safe_convert_for_json(value) for key, value in obj.items()}
|
| 321 |
-
elif isinstance(obj, list):
|
| 322 |
-
return [self._safe_convert_for_json(item) for item in obj]
|
| 323 |
-
elif isinstance(obj, pd.Timestamp):
|
| 324 |
-
return obj.isoformat()
|
| 325 |
-
else:
|
| 326 |
-
return obj
|
| 327 |
-
|
| 328 |
-
def _build_success_response(self) -> Dict[str, Any]:
|
| 329 |
-
"""Build a consistent success response"""
|
| 330 |
-
data_points = len(self.processed_glucose_data) if self.processed_glucose_data is not None else 0
|
| 331 |
-
avg_glucose = self.calculated_stats.get('average_glucose', 0)
|
| 332 |
-
time_in_range = self.calculated_stats.get('time_in_range_70_180', 0)
|
| 333 |
-
|
| 334 |
-
return {
|
| 335 |
-
"success": True,
|
| 336 |
-
"message": f"✅ Successfully loaded data for {self.current_user.name}",
|
| 337 |
-
"user": asdict(self.current_user),
|
| 338 |
-
"data_points": data_points,
|
| 339 |
-
"stats": self.calculated_stats,
|
| 340 |
-
"data_source": self.data_source,
|
| 341 |
-
"summary": f"📊 {data_points} readings | Avg: {avg_glucose:.1f} mg/dL | TIR: {time_in_range:.1f}% | Source: {self.data_source}"
|
| 342 |
-
}
|
| 343 |
-
|
| 344 |
-
def validate_data_consistency(self) -> Dict[str, Any]:
|
| 345 |
-
"""Validate that all components are using consistent data"""
|
| 346 |
-
if not self.calculated_stats:
|
| 347 |
-
return {"valid": False, "message": "No data loaded"}
|
| 348 |
-
|
| 349 |
-
validation = {
|
| 350 |
-
"valid": True,
|
| 351 |
-
"data_source": self.data_source,
|
| 352 |
-
"data_age_minutes": int((datetime.now() - self.data_loaded_at).total_seconds() / 60) if self.data_loaded_at else None,
|
| 353 |
-
"total_readings": self.calculated_stats.get('total_readings', 0),
|
| 354 |
-
"time_in_range": self.calculated_stats.get('time_in_range_70_180', 0),
|
| 355 |
-
"average_glucose": self.calculated_stats.get('average_glucose', 0),
|
| 356 |
-
"user": self.current_user.name if self.current_user else None
|
| 357 |
-
}
|
| 358 |
-
|
| 359 |
-
logger.info(f"Data consistency check: {validation}")
|
| 360 |
-
|
| 361 |
-
return validation
|
| 362 |
-
|
| 363 |
-
# ADDITIONAL: Debug function to test the API connection as it was working before
|
| 364 |
-
def test_original_api_method():
|
| 365 |
-
"""Test the API exactly as it was working before unified data manager"""
|
| 366 |
-
from apifunctions import DexcomAPI, DEMO_USERS
|
| 367 |
-
|
| 368 |
-
print("🔍 Testing API exactly as it was working before...")
|
| 369 |
-
|
| 370 |
-
api = DexcomAPI()
|
| 371 |
-
|
| 372 |
-
# Test with sarah_g7 as it was working before
|
| 373 |
-
user_key = "sarah_g7"
|
| 374 |
-
user = DEMO_USERS[user_key]
|
| 375 |
-
|
| 376 |
-
print(f"Testing with {user.name} ({user.username})")
|
| 377 |
-
|
| 378 |
-
try:
|
| 379 |
-
# Call simulate_demo_login exactly as before
|
| 380 |
-
access_token = api.simulate_demo_login(user_key)
|
| 381 |
-
print(f"✅ Authentication: {bool(access_token)}")
|
| 382 |
-
|
| 383 |
-
if access_token:
|
| 384 |
-
# Call get_egv_data exactly as before
|
| 385 |
-
end_date = datetime.now()
|
| 386 |
-
start_date = end_date - timedelta(days=14)
|
| 387 |
-
|
| 388 |
-
egv_data = api.get_egv_data(
|
| 389 |
-
start_date.isoformat(),
|
| 390 |
-
end_date.isoformat()
|
| 391 |
-
)
|
| 392 |
-
|
| 393 |
-
print(f"✅ EGV Data: {len(egv_data)} readings")
|
| 394 |
-
|
| 395 |
-
if egv_data:
|
| 396 |
-
print(f"✅ SUCCESS! API is working as before")
|
| 397 |
-
sample = egv_data[0] if egv_data else {}
|
| 398 |
-
print(f"Sample reading: {sample}")
|
| 399 |
-
return True
|
| 400 |
-
else:
|
| 401 |
-
print("⚠️ API authenticated but returned no data")
|
| 402 |
-
return False
|
| 403 |
-
else:
|
| 404 |
-
print("❌ Authentication failed")
|
| 405 |
-
return False
|
| 406 |
-
|
| 407 |
-
except Exception as e:
|
| 408 |
-
print(f"❌ Error: {e}")
|
| 409 |
-
return False
|
| 410 |
-
|
| 411 |
-
if __name__ == "__main__":
|
| 412 |
-
# Test the original API method
|
| 413 |
-
test_original_api_method()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|