CoolFace
Apppublic

blackopsrepl/vehicle-routing-python

sourceHugging Faceapache-2.0updated 10mo agoView on Hugging Face
2likes
test_demo_data.py281 linesDownload Raw Back to tests
1"""2Tests for demo data generation with customer-type based time windows.3 4These tests verify that the demo data correctly generates realistic5delivery scenarios with customer types driving time windows and demand.6"""7import pytest8from datetime import time9 10from vehicle_routing.demo_data import (11    DemoData,12    generate_demo_data,13    CustomerType,14    random_customer_type,15    CUSTOMER_TYPE_WEIGHTS,16)17from random import Random18 19 20class TestCustomerTypes:21    """Tests for customer type definitions and selection."""22 23    def test_customer_types_have_valid_time_windows(self):24        """Each customer type should have a valid time window."""25        for ctype in CustomerType:26            assert ctype.window_start < ctype.window_end, (27                f"{ctype.name} window_start should be before window_end"28            )29 30    def test_customer_types_have_valid_demand_ranges(self):31        """Each customer type should have valid demand ranges."""32        for ctype in CustomerType:33            assert ctype.min_demand >= 1, f"{ctype.name} min_demand should be >= 1"34            assert ctype.max_demand >= ctype.min_demand, (35                f"{ctype.name} max_demand should be >= min_demand"36            )37 38    def test_customer_types_have_valid_service_duration_ranges(self):39        """Each customer type should have valid service duration ranges."""40        for ctype in CustomerType:41            assert ctype.min_service_minutes >= 1, (42                f"{ctype.name} min_service_minutes should be >= 1"43            )44            assert ctype.max_service_minutes >= ctype.min_service_minutes, (45                f"{ctype.name} max_service_minutes should be >= min_service_minutes"46            )47 48    def test_residential_time_window(self):49        """Residential customers have evening windows."""50        res = CustomerType.RESIDENTIAL51        assert res.window_start == time(17, 0)52        assert res.window_end == time(20, 0)53 54    def test_business_time_window(self):55        """Business customers have standard business hours."""56        biz = CustomerType.BUSINESS57        assert biz.window_start == time(9, 0)58        assert biz.window_end == time(17, 0)59 60    def test_restaurant_time_window(self):61        """Restaurant customers have early morning windows."""62        rest = CustomerType.RESTAURANT63        assert rest.window_start == time(6, 0)64        assert rest.window_end == time(10, 0)65 66    def test_weighted_selection_distribution(self):67        """Weighted selection should roughly match configured weights."""68        random = Random(42)69        counts = {ctype: 0 for ctype in CustomerType}70 71        n_samples = 1000072        for _ in range(n_samples):73            ctype = random_customer_type(random)74            counts[ctype] += 175 76        # Expected: 50% residential, 30% business, 20% restaurant77        total_weight = sum(w for _, w in CUSTOMER_TYPE_WEIGHTS)78        for ctype, weight in CUSTOMER_TYPE_WEIGHTS:79            expected_pct = weight / total_weight80            actual_pct = counts[ctype] / n_samples81            # Allow 5% tolerance82            assert abs(actual_pct - expected_pct) < 0.05, (83                f"{ctype.name}: expected {expected_pct:.2%}, got {actual_pct:.2%}"84            )85 86 87class TestDemoDataGeneration:88    """Tests for the demo data generation."""89 90    @pytest.mark.parametrize("demo", list(DemoData))91    def test_generates_correct_number_of_vehicles(self, demo):92        """Should generate the configured number of vehicles."""93        plan = generate_demo_data(demo)94        assert len(plan.vehicles) == demo.value.vehicle_count95 96    @pytest.mark.parametrize("demo", list(DemoData))97    def test_generates_correct_number_of_visits(self, demo):98        """Should generate the configured number of visits."""99        plan = generate_demo_data(demo)100        assert len(plan.visits) == demo.value.visit_count101 102    @pytest.mark.parametrize("demo", list(DemoData))103    def test_visits_have_valid_time_windows(self, demo):104        """All visits should have time windows matching customer types."""105        plan = generate_demo_data(demo)106        valid_windows = {107            (ctype.window_start, ctype.window_end) for ctype in CustomerType108        }109 110        for visit in plan.visits:111            window = (visit.min_start_time.time(), visit.max_end_time.time())112            assert window in valid_windows, (113                f"Visit {visit.id} has invalid window {window}"114            )115 116    @pytest.mark.parametrize("demo", list(DemoData))117    def test_visits_have_varied_time_windows(self, demo):118        """Visits should have a mix of different time windows."""119        plan = generate_demo_data(demo)120 121        windows = {122            (v.min_start_time.time(), v.max_end_time.time())123            for v in plan.visits124        }125 126        # Should have at least 2 different window types (likely all 3)127        assert len(windows) >= 2, "Should have varied time windows"128 129    @pytest.mark.parametrize("demo", list(DemoData))130    def test_vehicles_depart_at_6am(self, demo):131        """Vehicles should depart at 06:00 to serve restaurant customers."""132        plan = generate_demo_data(demo)133 134        for vehicle in plan.vehicles:135            assert vehicle.departure_time.hour == 6136            assert vehicle.departure_time.minute == 0137 138    @pytest.mark.parametrize("demo", list(DemoData))139    def test_visits_within_geographic_bounds(self, demo):140        """All visits should be within the specified geographic bounds."""141        plan = generate_demo_data(demo)142        sw = plan.south_west_corner143        ne = plan.north_east_corner144 145        for visit in plan.visits:146            assert sw.latitude <= visit.location.latitude <= ne.latitude, (147                f"Visit {visit.id} latitude {visit.location.latitude} "148                f"outside bounds [{sw.latitude}, {ne.latitude}]"149            )150            assert sw.longitude <= visit.location.longitude <= ne.longitude, (151                f"Visit {visit.id} longitude {visit.location.longitude} "152                f"outside bounds [{sw.longitude}, {ne.longitude}]"153            )154 155    @pytest.mark.parametrize("demo", list(DemoData))156    def test_vehicles_within_geographic_bounds(self, demo):157        """All vehicle home locations should be within geographic bounds."""158        plan = generate_demo_data(demo)159        sw = plan.south_west_corner160        ne = plan.north_east_corner161 162        for vehicle in plan.vehicles:163            loc = vehicle.home_location164            assert sw.latitude <= loc.latitude <= ne.latitude165            assert sw.longitude <= loc.longitude <= ne.longitude166 167    @pytest.mark.parametrize("demo", list(DemoData))168    def test_service_durations_match_customer_types(self, demo):169        """Service durations should match their customer type's service duration range."""170        plan = generate_demo_data(demo)171 172        # Map time windows back to customer types173        window_to_type = {174            (ctype.window_start, ctype.window_end): ctype175            for ctype in CustomerType176        }177 178        for visit in plan.visits:179            window = (visit.min_start_time.time(), visit.max_end_time.time())180            ctype = window_to_type[window]181            duration_minutes = int(visit.service_duration.total_seconds() / 60)182            assert ctype.min_service_minutes <= duration_minutes <= ctype.max_service_minutes, (183                f"Visit {visit.id} ({ctype.name}) service duration {duration_minutes}min "184                f"outside [{ctype.min_service_minutes}, {ctype.max_service_minutes}]"185            )186 187    @pytest.mark.parametrize("demo", list(DemoData))188    def test_demands_match_customer_types(self, demo):189        """Visit demands should match their customer type's demand range."""190        plan = generate_demo_data(demo)191 192        # Map time windows back to customer types193        window_to_type = {194            (ctype.window_start, ctype.window_end): ctype195            for ctype in CustomerType196        }197 198        for visit in plan.visits:199            window = (visit.min_start_time.time(), visit.max_end_time.time())200            ctype = window_to_type[window]201            assert ctype.min_demand <= visit.demand <= ctype.max_demand, (202                f"Visit {visit.id} ({ctype.name}) demand {visit.demand} "203                f"outside [{ctype.min_demand}, {ctype.max_demand}]"204            )205 206    @pytest.mark.parametrize("demo", list(DemoData))207    def test_vehicle_capacities_within_bounds(self, demo):208        """Vehicle capacities should be within configured bounds."""209        plan = generate_demo_data(demo)210        props = demo.value211 212        for vehicle in plan.vehicles:213            assert props.min_vehicle_capacity <= vehicle.capacity <= props.max_vehicle_capacity, (214                f"Vehicle {vehicle.id} capacity {vehicle.capacity} "215                f"outside [{props.min_vehicle_capacity}, {props.max_vehicle_capacity}]"216            )217 218    @pytest.mark.parametrize("demo", list(DemoData))219    def test_deterministic_with_same_seed(self, demo):220        """Same demo data should produce identical results (deterministic)."""221        plan1 = generate_demo_data(demo)222        plan2 = generate_demo_data(demo)223 224        assert len(plan1.visits) == len(plan2.visits)225        assert len(plan1.vehicles) == len(plan2.vehicles)226 227        for v1, v2 in zip(plan1.visits, plan2.visits):228            assert v1.location.latitude == v2.location.latitude229            assert v1.location.longitude == v2.location.longitude230            assert v1.demand == v2.demand231            assert v1.service_duration == v2.service_duration232            assert v1.min_start_time == v2.min_start_time233            assert v1.max_end_time == v2.max_end_time234 235 236class TestHaversineIntegration:237    """Tests verifying Haversine distance is used correctly in demo data."""238 239    def test_philadelphia_diagonal_realistic(self):240        """Philadelphia area diagonal should be ~15km with Haversine (tightened bbox)."""241        props = DemoData.PHILADELPHIA.value242        diagonal_seconds = props.south_west_corner.driving_time_to(243            props.north_east_corner244        )245        diagonal_km = (diagonal_seconds / 3600) * 50  # 50 km/h average246 247        # Philadelphia bbox is tightened to Center City area (~8km x 12km)248        # Diagonal should be around 10-20km249        assert 8 < diagonal_km < 25, f"Diagonal {diagonal_km}km seems wrong"250 251    def test_firenze_diagonal_realistic(self):252        """Firenze area diagonal should be ~10km with Haversine."""253        props = DemoData.FIRENZE.value254        diagonal_seconds = props.south_west_corner.driving_time_to(255            props.north_east_corner256        )257        diagonal_km = (diagonal_seconds / 3600) * 50  # 50 km/h average258 259        # Firenze area is small, roughly 6km x 12km260        assert 5 < diagonal_km < 20, f"Diagonal {diagonal_km}km seems wrong"261 262    def test_inter_visit_distances_use_haversine(self):263        """Distances between visits should use Haversine formula."""264        plan = generate_demo_data(DemoData.PHILADELPHIA)265 266        # Pick two visits267        v1, v2 = plan.visits[0], plan.visits[1]268 269        # Calculate distance using the Location method270        haversine_time = v1.location.driving_time_to(v2.location)271 272        # Verify it's not using simple Euclidean (which would be ~4000 * coord_diff)273        simple_euclidean = round(274            ((v1.location.latitude - v2.location.latitude) ** 2 +275             (v1.location.longitude - v2.location.longitude) ** 2) ** 0.5 * 4000276        )277 278        # Haversine should give different (usually larger) results279        # for geographic coordinates280        assert haversine_time != simple_euclidean or haversine_time == 0281