comparison.py 7,95 ko
Newer Older
"""
Comparison utilities for analytical vs simulation results.
"""
from typing import Dict, Tuple
from dataclasses import dataclass

from .jackson import JacksonAnalyzer, NetworkAnalytics
from ..core.simulation import SimulationResults


@dataclass
class QueueComparison:
    """Comparison of analytical vs simulation for a single queue."""
    queue_id: str

    # Analytical
    analytical_utilization: float
    analytical_avg_customers: float
    analytical_avg_time: float
    analytical_avg_wait: float

    # Simulation
    simulation_utilization: float
    simulation_avg_customers: float
    simulation_avg_time: float
    simulation_avg_wait: float

    # Differences (%)
    utilization_diff_percent: float
    avg_customers_diff_percent: float
    avg_time_diff_percent: float
    avg_wait_diff_percent: float


@dataclass
class NetworkComparison:
    """Complete comparison of analytical vs simulation results."""
    is_stable: bool
    coordinator: QueueComparison
    servers: Dict[str, QueueComparison]

    # System-wide
    analytical_total_L: float
    analytical_total_W: float
    simulation_total_L: float
    simulation_total_W: float

    total_L_diff_percent: float
    total_W_diff_percent: float


def calculate_percentage_difference(analytical: float, simulation: float) -> float:
    """
    Calculate percentage difference: ((simulation - analytical) / analytical) * 100

    Args:
        analytical: Analytical value
        simulation: Simulation value

    Returns:
        Percentage difference
    """
    if analytical == 0:
        return 0.0 if simulation == 0 else 100.0

    return ((simulation - analytical) / analytical) * 100.0


def compare_queue(
    queue_id: str,
    analytical: 'QueueAnalytics',
    simulation: Dict
) -> QueueComparison:
    """
    Compare analytical vs simulation results for a single queue.

    Args:
        queue_id: Queue identifier
        analytical: QueueAnalytics from Jackson's theorem
        simulation: Simulation statistics dict

    Returns:
        QueueComparison with all metrics
    """
    # Handle unstable queues
    if not analytical.is_stable:
        analytical_L = float('inf')
        analytical_W = float('inf')
        analytical_Wq = float('inf')
    else:
        analytical_L = analytical.average_customers
        analytical_W = analytical.average_time
        analytical_Wq = analytical.average_wait_time

    simulation_rho = simulation['utilization']
    simulation_W = simulation['average_system_time']
    simulation_Wq = simulation['average_wait_time']

    # Estimate L from simulation using Little's Law: L = λ * W
    simulation_L = analytical.arrival_rate * simulation_W if analytical.arrival_rate > 0 else 0

    return QueueComparison(
        queue_id=queue_id,
        analytical_utilization=analytical.utilization,
        analytical_avg_customers=analytical_L,
        analytical_avg_time=analytical_W,
        analytical_avg_wait=analytical_Wq,
        simulation_utilization=simulation_rho,
        simulation_avg_customers=simulation_L,
        simulation_avg_time=simulation_W,
        simulation_avg_wait=simulation_Wq,
        utilization_diff_percent=calculate_percentage_difference(
            analytical.utilization, simulation_rho
        ),
        avg_customers_diff_percent=calculate_percentage_difference(
            analytical_L, simulation_L
        ),
        avg_time_diff_percent=calculate_percentage_difference(
            analytical_W, simulation_W
        ),
        avg_wait_diff_percent=calculate_percentage_difference(
            analytical_Wq, simulation_Wq
        )
    )


def compare_results(
    analytical: NetworkAnalytics,
    simulation: SimulationResults
) -> NetworkComparison:
    """
    Compare complete analytical vs simulation results.

    Args:
        analytical: NetworkAnalytics from Jackson's theorem
        simulation: SimulationResults from simulation

    Returns:
        NetworkComparison with all metrics
    """
    # Compare coordinator
    coordinator_comp = compare_queue(
        "coordinator",
        analytical.coordinator,
        simulation.coordinator_stats
    )

    # Compare servers
    server_comps = {}
    for server_id, server_analytical in analytical.servers.items():
        server_simulation = simulation.server_stats[server_id]
        server_comps[server_id] = compare_queue(
            server_id,
            server_analytical,
            server_simulation
        )

    # System-wide comparison
    simulation_total_W = simulation.average_system_time

    # Estimate total L using Little's Law: L = λ₀ × W
    # IMPORTANT: Use external arrival rate λ₀, NOT coordinator effective rate γ₀
    external_lambda = analytical.external_arrival_rate
    simulation_total_L = external_lambda * simulation_total_W if external_lambda > 0 else 0

    return NetworkComparison(
        is_stable=analytical.is_stable,
        coordinator=coordinator_comp,
        servers=server_comps,
        analytical_total_L=analytical.total_average_customers,
        analytical_total_W=analytical.total_average_time,
        simulation_total_L=simulation_total_L,
        simulation_total_W=simulation_total_W,
        total_L_diff_percent=calculate_percentage_difference(
            analytical.total_average_customers,
            simulation_total_L
        ),
        total_W_diff_percent=calculate_percentage_difference(
            analytical.total_average_time,
            simulation_total_W
        )
    )


def print_comparison(comparison: NetworkComparison) -> None:
    """Print a formatted comparison report."""
    print("=" * 80)
    print("ANALYTICAL VS SIMULATION COMPARISON")
    print("=" * 80)

    print(f"\n🌐 System-Wide Metrics:")
    print(f"  {'Metric':<20} {'Analytical':<15} {'Simulation':<15} {'Diff %':<10}")
    print("  " + "-" * 60)
    print(f"  {'Total L (customers)':<20} {comparison.analytical_total_L:<15.4f} "
          f"{comparison.simulation_total_L:<15.4f} {comparison.total_L_diff_percent:>9.2f}%")
    print(f"  {'Total W (time)':<20} {comparison.analytical_total_W:<15.4f} "
          f"{comparison.simulation_total_W:<15.4f} {comparison.total_W_diff_percent:>9.2f}%")

    print(f"\n📊 Coordinator Queue:")
    coord = comparison.coordinator
    print(f"  {'Metric':<20} {'Analytical':<15} {'Simulation':<15} {'Diff %':<10}")
    print("  " + "-" * 60)
    print(f"  {'Utilization (ρ)':<20} {coord.analytical_utilization:<15.4f} "
          f"{coord.simulation_utilization:<15.4f} {coord.utilization_diff_percent:>9.2f}%")
    print(f"  {'Avg customers (L)':<20} {coord.analytical_avg_customers:<15.4f} "
          f"{coord.simulation_avg_customers:<15.4f} {coord.avg_customers_diff_percent:>9.2f}%")
    print(f"  {'Avg time (W)':<20} {coord.analytical_avg_time:<15.4f} "
          f"{coord.simulation_avg_time:<15.4f} {coord.avg_time_diff_percent:>9.2f}%")
    print(f"  {'Avg wait (Wq)':<20} {coord.analytical_avg_wait:<15.4f} "
          f"{coord.simulation_avg_wait:<15.4f} {coord.avg_wait_diff_percent:>9.2f}%")

    for server_id, server_comp in comparison.servers.items():
        print(f"\n📊 {server_id}:")
        print(f"  {'Metric':<20} {'Analytical':<15} {'Simulation':<15} {'Diff %':<10}")
        print("  " + "-" * 60)
        print(f"  {'Utilization (ρ)':<20} {server_comp.analytical_utilization:<15.4f} "
              f"{server_comp.simulation_utilization:<15.4f} {server_comp.utilization_diff_percent:>9.2f}%")
        print(f"  {'Avg customers (L)':<20} {server_comp.analytical_avg_customers:<15.4f} "
              f"{server_comp.simulation_avg_customers:<15.4f} {server_comp.avg_customers_diff_percent:>9.2f}%")
        print(f"  {'Avg time (W)':<20} {server_comp.analytical_avg_time:<15.4f} "
              f"{server_comp.simulation_avg_time:<15.4f} {server_comp.avg_time_diff_percent:>9.2f}%")
        print(f"  {'Avg wait (Wq)':<20} {server_comp.analytical_avg_wait:<15.4f} "
              f"{server_comp.simulation_avg_wait:<15.4f} {server_comp.avg_wait_diff_percent:>9.2f}%")

    print("\n" + "=" * 80)
    print("✅ Comparison complete")
    print("=" * 80)