analytics.py 10,6 ko
Newer Older
"""
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))