""" API endpoints for analytical analysis (Jackson's theorem). """ from fastapi import APIRouter, HTTPException from pydantic import BaseModel from typing import Dict, Optional from ..models.config import SimulationConfigModel from ..analytics.jackson import JacksonAnalyzer, QueueAnalytics, NetworkAnalytics from ..analytics.comparison import compare_results from .simulation import simulation_sessions router = APIRouter(prefix="/api/analytics", tags=["analytics"]) class QueueAnalyticsResponse(BaseModel): """Analytical results for a single queue.""" queue_id: str service_rate: float arrival_rate: float utilization: float is_stable: bool average_customers: Optional[float] = None average_time: Optional[float] = None average_wait_time: Optional[float] = None class NetworkAnalyticsResponse(BaseModel): """Analytical results for the entire network.""" is_stable: bool coordinator: QueueAnalyticsResponse servers: Dict[str, QueueAnalyticsResponse] total_average_customers: float total_average_time: float external_arrival_rate: float # λ₀ instability_reason: Optional[str] = None class ComparisonMetric(BaseModel): """Comparison of a single metric.""" analytical: float simulation: float difference_percent: float class QueueComparisonResponse(BaseModel): """Comparison for a single queue.""" queue_id: str utilization: ComparisonMetric average_customers: ComparisonMetric average_time: ComparisonMetric average_wait: ComparisonMetric class NetworkComparisonResponse(BaseModel): """Complete comparison response.""" is_stable: bool coordinator: QueueComparisonResponse servers: Dict[str, QueueComparisonResponse] total_L: ComparisonMetric total_W: ComparisonMetric @router.post("/jackson", response_model=NetworkAnalyticsResponse) async def analyze_with_jackson(config: SimulationConfigModel): """ Analyze a configuration using Jackson's theorem. Args: config: Network configuration Returns: Analytical results (L, W, stability, etc.) """ try: # Create analyzer analyzer = JacksonAnalyzer( external_arrival_rate=config.arrival_rate, coordinator_service_rate=config.coordinator_service_rate, coordinator_exit_prob=config.coordinator_exit_probability, server_service_rates=[s.service_rate for s in config.servers], server_routing_probs=[s.routing_probability for s in config.servers] ) # Perform analysis results = analyzer.analyze() # Convert to response model coordinator_response = QueueAnalyticsResponse( queue_id="coordinator", service_rate=results.coordinator.service_rate, arrival_rate=results.coordinator.arrival_rate, utilization=results.coordinator.utilization, is_stable=results.coordinator.is_stable, average_customers=results.coordinator.average_customers if results.coordinator.is_stable else None, average_time=results.coordinator.average_time if results.coordinator.is_stable else None, average_wait_time=results.coordinator.average_wait_time if results.coordinator.is_stable else None ) servers_response = {} for server_id, server_analytics in results.servers.items(): servers_response[server_id] = QueueAnalyticsResponse( queue_id=server_id, service_rate=server_analytics.service_rate, arrival_rate=server_analytics.arrival_rate, utilization=server_analytics.utilization, is_stable=server_analytics.is_stable, average_customers=server_analytics.average_customers if server_analytics.is_stable else None, average_time=server_analytics.average_time if server_analytics.is_stable else None, average_wait_time=server_analytics.average_wait_time if server_analytics.is_stable else None ) # Handle infinity values for unstable systems import math total_L = results.total_average_customers if not math.isinf(results.total_average_customers) else 0 total_W = results.total_average_time if not math.isinf(results.total_average_time) else 0 return NetworkAnalyticsResponse( is_stable=results.is_stable, coordinator=coordinator_response, servers=servers_response, total_average_customers=total_L, total_average_time=total_W, external_arrival_rate=results.external_arrival_rate, instability_reason=results.instability_reason ) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @router.get("/compare/{session_id}", response_model=NetworkComparisonResponse) async def compare_simulation_with_analytical(session_id: str): """ Compare simulation results with analytical predictions. Args: session_id: Simulation session identifier Returns: Comparison of analytical vs simulation results """ if session_id not in simulation_sessions: raise HTTPException(status_code=404, detail="Session not found") try: session = simulation_sessions[session_id] config_dict = session["config"] simulation_results = session["results"] # Create analyzer from stored config server_service_rates = [s["service_rate"] for s in config_dict["servers"]] server_routing_probs = [s["routing_probability"] for s in config_dict["servers"]] analyzer = JacksonAnalyzer( external_arrival_rate=config_dict["arrival_rate"], coordinator_service_rate=config_dict["coordinator_service_rate"], coordinator_exit_prob=config_dict["coordinator_exit_probability"], server_service_rates=server_service_rates, server_routing_probs=server_routing_probs ) analytical_results = analyzer.analyze() # Perform comparison comparison = compare_results(analytical_results, simulation_results) # Convert to response model def make_comparison_metric(analytical: float, simulation: float, diff_percent: float) -> ComparisonMetric: return ComparisonMetric( analytical=analytical, simulation=simulation, difference_percent=diff_percent ) coordinator_comp = QueueComparisonResponse( queue_id="coordinator", utilization=make_comparison_metric( comparison.coordinator.analytical_utilization, comparison.coordinator.simulation_utilization, comparison.coordinator.utilization_diff_percent ), average_customers=make_comparison_metric( comparison.coordinator.analytical_avg_customers, comparison.coordinator.simulation_avg_customers, comparison.coordinator.avg_customers_diff_percent ), average_time=make_comparison_metric( comparison.coordinator.analytical_avg_time, comparison.coordinator.simulation_avg_time, comparison.coordinator.avg_time_diff_percent ), average_wait=make_comparison_metric( comparison.coordinator.analytical_avg_wait, comparison.coordinator.simulation_avg_wait, comparison.coordinator.avg_wait_diff_percent ) ) servers_comp = {} for server_id, server_comparison in comparison.servers.items(): servers_comp[server_id] = QueueComparisonResponse( queue_id=server_id, utilization=make_comparison_metric( server_comparison.analytical_utilization, server_comparison.simulation_utilization, server_comparison.utilization_diff_percent ), average_customers=make_comparison_metric( server_comparison.analytical_avg_customers, server_comparison.simulation_avg_customers, server_comparison.avg_customers_diff_percent ), average_time=make_comparison_metric( server_comparison.analytical_avg_time, server_comparison.simulation_avg_time, server_comparison.avg_time_diff_percent ), average_wait=make_comparison_metric( server_comparison.analytical_avg_wait, server_comparison.simulation_avg_wait, server_comparison.avg_wait_diff_percent ) ) return NetworkComparisonResponse( is_stable=comparison.is_stable, coordinator=coordinator_comp, servers=servers_comp, total_L=make_comparison_metric( comparison.analytical_total_L, comparison.simulation_total_L, comparison.total_L_diff_percent ), total_W=make_comparison_metric( comparison.analytical_total_W, comparison.simulation_total_W, comparison.total_W_diff_percent ) ) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @router.post("/stability") async def check_stability(config: SimulationConfigModel): """ Check if a configuration is stable using Jackson's theorem. Args: config: Network configuration Returns: Stability information """ try: analyzer = JacksonAnalyzer( external_arrival_rate=config.arrival_rate, coordinator_service_rate=config.coordinator_service_rate, coordinator_exit_prob=config.coordinator_exit_probability, server_service_rates=[s.service_rate for s in config.servers], server_routing_probs=[s.routing_probability for s in config.servers] ) utilizations = analyzer.calculate_utilizations( analyzer.calculate_effective_arrival_rates() ) is_stable, reason = analyzer.check_stability(utilizations) conditions = [] for queue_id, rho in utilizations.items(): conditions.append({ "queue": queue_id, "utilization": rho, "stable": rho < 1.0 }) return { "is_stable": is_stable, "conditions": conditions, "reason": reason } except Exception as e: raise HTTPException(status_code=400, detail=str(e))