File size: 19,395 Bytes
62e0a0e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fec5550
62e0a0e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fec5550
 
62e0a0e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fec5550
 
 
 
 
 
 
 
 
62e0a0e
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
"""
AETERNA AI — Operational Logistics Engine
Centralized, deterministic mathematical service for municipal waste logistics in DKI Jakarta.

Architecture Principle:
    AI Forecast Model (Predicted Volume)

    Deterministic Logistics Engine (Physics & Municipal Standards)

    Fleet / Crew / Collection Time / Efficiency / Reliability Breakdown

    FastAPI Endpoints & Frontend Decision Intelligence Dashboard
"""

import math
from typing import Dict, Any, Optional

# ==========================================
# 1. CENTRALIZED LOGISTICS CONFIGURATION
# ==========================================
# Prototype Operational Assumptions. These values have not been validated against official DLH DKI Jakarta fleet specifications. Use for R&D and decision-support demonstration only.
LOGISTICS_CONFIG: Dict[str, Any] = {
    # Vehicle specifications
    "truck_capacity_ton": 15.0,        # Standard heavy compactor truck gross payload capacity
    "load_factor": 0.95,               # Operational target filling factor (prevent spillage/overload)
    "effective_capacity_per_truck": 15.0 * 0.95,  # 14.25 tons effective capacity per truck-load
    "operational_buffer": 0.05,        # 5% reserve fleet buffer for maintenance, breakdown, and surges

    # Crew staffing requirements per vehicle
    "crew": {
        "drivers_per_truck": 1,        # Certified heavy vehicle driver
        "collectors_per_truck": 2,      # Sanitarian collection crew / loader helpers
        "crew_per_truck": 3            # Total personnel per deployed truck
    },

    # Operational collection throughput baseline
    # Realistic municipal collection throughput in urban Indonesian residential/commercial zones:
    # 1 truck with 3 crew collects approximately 2.0 metric tons of municipal waste per working hour.
    "collection_rate_ton_per_hour": 2.0,

    # Environmental and urban traffic adjustments
    "factors": {
        "baseline_traffic": 1.10,      # Average urban transit congestion delay factor (+10%)
        "rush_hour_traffic": 1.25,     # Peak commercial/business hours delay factor (+25%)
        "normal_weather": 1.00,        # Clear / dry asphalt road condition
        "light_rain": 1.05,            # Precipitation 0.1 - 10.0 mm (+5% caution slowdown)
        "moderate_rain": 1.10,         # Precipitation 10.1 - 25.0 mm (+10% spray/drainage slowdown)
        "heavy_rain": 1.20,            # Precipitation > 25.0 mm (+20% hydroplaning/flooding slowdown)
        "no_event": 1.00,              # Standard daily routine operations
        "event_crowd": 1.10            # Mass public gathering / car free day / festival perimeter (+10%)
    },

    # Multi-factor weights for Operational Efficiency Score (Sum = 1.00)
    "weights_efficiency": {
        "fleet_adequacy": 0.35,        # 35%: Ratio of allocated capacity vs forecast demand
        "weather_condition": 0.20,     # 20%: Weather impact on transit and loading speed
        "traffic_condition": 0.20,     # 20%: Road network throughput & traffic speed
        "event_impact": 0.15,          # 15%: Localized public events congestion
        "capacity_utilization": 0.10   # 10%: Proximity to optimal payload load factor
    },

    # Weights for Forecast Reliability Score (Confidence Score) (Sum = 1.00)
    "weights_reliability": {
        "model_quality": 0.50,         # 50%: Historical out-of-sample MAPE / R2 performance
        "data_completeness": 0.30,     # 30%: Weather API live stream & BPS demographic verification
        "horizon_penalty": 0.20        # 20%: Uncertainty propagation over forecast horizon days
    }
}


def calculate_fleet_requirements(
    forecast_volume_ton: float,
    truck_capacity_ton: Optional[float] = None,
    load_factor: Optional[float] = None,
    operational_buffer: Optional[float] = None
) -> Dict[str, Any]:
    """
    Calculate transparent recommended fleet sizing based on forecast tonnage.

    Formula:
        truck_loads = forecast_volume_ton / truck_capacity_ton
        effective_capacity = truck_capacity_ton * load_factor
        base_trucks = ceil(forecast_volume_ton / effective_capacity)
        recommended_trucks = ceil(base_trucks * (1 + operational_buffer))
    """
    cap = float(truck_capacity_ton if truck_capacity_ton is not None else LOGISTICS_CONFIG["truck_capacity_ton"])
    lf = float(load_factor if load_factor is not None else LOGISTICS_CONFIG["load_factor"])
    buf = float(operational_buffer if operational_buffer is not None else LOGISTICS_CONFIG["operational_buffer"])

    effective_cap = cap * lf

    if forecast_volume_ton <= 0.0:
        return {
            "truck_capacity_ton": round(cap, 1),
            "load_factor": round(lf, 2),
            "effective_capacity_ton": round(effective_cap, 2),
            "base_trucks": 0,
            "operational_buffer_percent": round(buf * 100, 1),
            "recommended_trucks": 0,
            "required_truck_loads": 0.0
        }

    truck_loads = forecast_volume_ton / cap
    base_trucks = math.ceil(forecast_volume_ton / effective_cap)
    recommended_trucks = math.ceil(base_trucks * (1.0 + buf))

    return {
        "truck_capacity_ton": round(cap, 1),
        "load_factor": round(lf, 2),
        "effective_capacity_ton": round(effective_cap, 2),
        "base_trucks": base_trucks,
        "operational_buffer_percent": round(buf * 100, 1),
        "recommended_trucks": recommended_trucks,
        "required_truck_loads": round(truck_loads, 2)
    }


def calculate_manpower_requirements(
    recommended_trucks: int,
    drivers_per_truck: Optional[int] = None,
    collectors_per_truck: Optional[int] = None
) -> Dict[str, Any]:
    """
    Derive exact staffing headcount from active fleet requirements.

    Formula:
        crew_per_truck = drivers_per_truck + collectors_per_truck (3)
        total_personnel = recommended_trucks * crew_per_truck
    """
    drivers_unit = int(drivers_per_truck if drivers_per_truck is not None else LOGISTICS_CONFIG["crew"]["drivers_per_truck"])
    collectors_unit = int(collectors_per_truck if collectors_per_truck is not None else LOGISTICS_CONFIG["crew"]["collectors_per_truck"])
    crew_unit = drivers_unit + collectors_unit

    if recommended_trucks <= 0:
        return {
            "drivers": 0,
            "collectors": 0,
            "total_personnel": 0,
            "crew_per_truck": crew_unit
        }

    drivers = recommended_trucks * drivers_unit
    collectors = recommended_trucks * collectors_unit
    total_personnel = recommended_trucks * crew_unit

    return {
        "drivers": drivers,
        "collectors": collectors,
        "total_personnel": total_personnel,
        "crew_per_truck": crew_unit
    }


def calculate_collection_time(
    forecast_volume_ton: float,
    recommended_trucks: int,
    collection_rate_per_truck: Optional[float] = None,
    rainfall_mm: float = 0.0,
    has_event: bool = False,
    is_rush_hour: bool = False,
    operational_efficiency: float = 0.85
) -> Dict[str, Any]:
    """
    Calculate estimated collection duration based on fleet throughput, NOT volume / truck capacity.

    Concept:
        Fleet Throughput = Number of Trucks * Collection Rate per Truck (ton/hour)
        Raw Collection Hours = Forecast Volume / Fleet Throughput
        Adjustment Factor = Traffic Factor * Weather Factor * Event Factor
        Adjusted Hours = (Raw Hours * Adjustment Factor) / Operational Efficiency

    Trip Logic:
        If truck_loads > recommended_trucks, multiple trips per truck are required.
        Additional transit turnaround time is incorporated for multi-trip logistics cycles.
    """
    rate = float(collection_rate_per_truck if collection_rate_per_truck is not None else LOGISTICS_CONFIG["collection_rate_ton_per_hour"])
    cap = float(LOGISTICS_CONFIG["truck_capacity_ton"])

    if forecast_volume_ton <= 0.0 or recommended_trucks <= 0:
        return {
            "raw_hours": 0.0,
            "adjusted_hours": 0.0,
            "collection_rate_ton_per_hour_per_truck": round(rate, 1),
            "average_trips_per_truck": 0.0,
            "factors": {
                "traffic_factor": 1.0,
                "weather_factor": 1.0,
                "event_factor": 1.0,
                "operational_efficiency": round(operational_efficiency, 2)
            }
        }

    # 1. Fleet Aggregate Throughput
    fleet_throughput_ton_per_hour = recommended_trucks * rate
    raw_hours = forecast_volume_ton / fleet_throughput_ton_per_hour

    # 2. Environmental Adjustment Factors
    factors_cfg = LOGISTICS_CONFIG["factors"]
    traffic_factor = factors_cfg["rush_hour_traffic"] if is_rush_hour else factors_cfg["baseline_traffic"]

    if rainfall_mm > 25.0:
        weather_factor = factors_cfg["heavy_rain"]
    elif rainfall_mm > 10.0:
        weather_factor = factors_cfg["moderate_rain"]
    elif rainfall_mm > 0.0:
        weather_factor = factors_cfg["light_rain"]
    else:
        weather_factor = factors_cfg["normal_weather"]

    event_factor = factors_cfg["event_crowd"] if has_event else factors_cfg["no_event"]

    # 3. Trip Logic: check if trips per vehicle exceed 1.0
    truck_loads = forecast_volume_ton / cap
    average_trips_per_truck = truck_loads / recommended_trucks

    # If trucks need to perform multiple turnaround trips, add standard 1.25 hours transit turnaround
    additional_trip_hours = 0.0
    if average_trips_per_truck > 1.0:
        additional_trips = average_trips_per_truck - 1.0
        additional_trip_hours = additional_trips * 1.25

    # 4. Composite Adjusted Collection Time
    eff = max(0.10, operational_efficiency)
    adjusted_hours = ((raw_hours + additional_trip_hours) * (traffic_factor * weather_factor * event_factor)) / eff

    return {
        "raw_hours": round(raw_hours, 1),
        "adjusted_hours": round(adjusted_hours, 1),
        "collection_rate_ton_per_hour_per_truck": round(rate, 1),
        "average_trips_per_truck": round(average_trips_per_truck, 2),
        "factors": {
            "traffic_factor": round(traffic_factor, 2),
            "weather_factor": round(weather_factor, 2),
            "event_factor": round(event_factor, 2),
            "operational_efficiency": round(eff, 2)
        }
    }


def calculate_operational_efficiency_score(
    recommended_trucks: int,
    forecast_volume_ton: float,
    rainfall_mm: float = 0.0,
    has_event: bool = False,
    is_rush_hour: bool = False
) -> Dict[str, Any]:
    """
    Calculate dynamic Operational Efficiency Score based on weighted real-world operational factors.

    Weights:
        Fleet Adequacy (35%)
        Weather Condition (20%)
        Traffic Condition (20%)
        Event Impact (15%)
        Capacity Utilization (10%)
    """
    weights = LOGISTICS_CONFIG["weights_efficiency"]
    cap = LOGISTICS_CONFIG["truck_capacity_ton"]
    effective_cap = LOGISTICS_CONFIG["effective_capacity_per_truck"]

    if forecast_volume_ton <= 0.0:
        return {
            "score_percent": 100.0,
            "status": "Optimal",
            "display": "100% — Optimal",
            "breakdown": {
                "fleet_adequacy": 100.0,
                "weather_condition": 100.0,
                "traffic_condition": 100.0,
                "event_impact": 100.0,
                "capacity_utilization": 100.0
            }
        }

    # 1. Fleet Adequacy Score (0-100)
    total_effective_allocated = recommended_trucks * effective_cap
    coverage_ratio = total_effective_allocated / forecast_volume_ton if forecast_volume_ton > 0 else 1.0
    if coverage_ratio >= 1.0:
        fleet_score = 100.0
    else:
        fleet_score = max(20.0, coverage_ratio * 100.0)

    # 2. Weather Condition Score (0-100)
    if rainfall_mm > 25.0:
        weather_score = 50.0   # Heavy rain / flood alert
    elif rainfall_mm > 10.0:
        weather_score = 75.0   # Moderate rain
    elif rainfall_mm > 0.0:
        weather_score = 90.0   # Light drizzle
    else:
        weather_score = 100.0  # Clear dry skies

    # 3. Traffic Condition Score (0-100)
    traffic_score = 70.0 if is_rush_hour else 90.0

    # 4. Event Impact Score (0-100)
    event_score = 75.0 if has_event else 100.0

    # 5. Capacity Utilization Score (0-100)
    if recommended_trucks > 0:
        utilization = (forecast_volume_ton / (recommended_trucks * cap)) * 100.0
        if 85.0 <= utilization <= 100.0:
            utilization_score = 100.0
        elif 70.0 <= utilization < 85.0:
            utilization_score = 88.0
        else:
            utilization_score = 75.0
    else:
        utilization_score = 50.0

    # Composite weighted efficiency score
    total_score = (
        fleet_score * weights["fleet_adequacy"] +
        weather_score * weights["weather_condition"] +
        traffic_score * weights["traffic_condition"] +
        event_score * weights["event_impact"] +
        utilization_score * weights["capacity_utilization"]
    )
    total_score = round(min(100.0, max(0.0, total_score)), 1)

    # Operational status classification
    if total_score >= 85.0:
        status = "Optimal"
    elif total_score >= 70.0:
        status = "Good"
    elif total_score >= 55.0:
        status = "Moderate"
    else:
        status = "High Operational Risk"

    return {
        "score_percent": total_score,
        "status": status,
        "display": f"{int(total_score)}% — {status}",
        "breakdown": {
            "fleet_adequacy": round(fleet_score, 1),
            "weather_condition": round(weather_score, 1),
            "traffic_condition": round(traffic_score, 1),
            "event_impact": round(event_score, 1),
            "capacity_utilization": round(utilization_score, 1)
        }
    }


def calculate_forecast_reliability_score(
    test_mape: float = 6.12,
    has_live_weather: bool = True,
    has_verified_bps: bool = True,
    forecast_days: int = 7
) -> Dict[str, Any]:
    """
    Calculate Forecast Reliability Score based on empirical model quality, data verification, and horizon decay.

    Formula:
        Reliability = Model Quality (50%) + Data Completeness (30%) + Horizon Score (20%)
    """
    weights = LOGISTICS_CONFIG["weights_reliability"]

    # 1. Model Quality Score based on out-of-sample MAPE
    # Model quality score based on out-of-sample MAPE from synthetic benchmark evaluation.
    # Note: test_mape=6.12 is a hardcoded fallback from a previous evaluation.
    model_quality = max(0.60, min(0.98, 1.0 - (test_mape / 100.0)))

    # 2. Data Completeness & Verification Score
    completeness = 1.0
    if not has_live_weather:
        completeness -= 0.15
    if not has_verified_bps:
        completeness -= 0.15
    completeness = max(0.70, completeness)

    # 3. Forecast Horizon Uncertainty Score
    if forecast_days <= 1:
        horizon_score = 1.00
    elif forecast_days <= 3:
        horizon_score = 0.96
    elif forecast_days <= 7:
        horizon_score = 0.92
    elif forecast_days <= 14:
        horizon_score = 0.85
    else:
        horizon_score = 0.78

    composite = (
        model_quality * weights["model_quality"] +
        completeness * weights["data_completeness"] +
        horizon_score * weights["horizon_penalty"]
    )
    reliability_pct = round(composite * 100.0, 1)

    return {
        "score_percent": reliability_pct,
        "display": f"{reliability_pct}%",
        "label": "Forecast Reliability Score",
        "breakdown": {
            "model_quality_score": round(model_quality * 100, 1),
            "data_completeness_score": round(completeness * 100, 1),
            "horizon_score": round(horizon_score * 100, 1)
        }
    }


def calculate_full_logistics_plan(
    total_forecast_volume_ton: float,
    forecast_days: int = 7,
    rainfall_mm: float = 0.0,
    has_event: bool = False,
    is_rush_hour: bool = False,
    test_mape: float = 6.12,
    has_live_weather: bool = True,
    has_verified_bps: bool = True
) -> Dict[str, Any]:
    """
    Generate the complete, unified Operational Logistics Plan for Aeterna AI.
    Integrates Fleet, Manpower, Throughput Collection Time, Dynamic Efficiency, and Reliability.
    """
    volume = round(float(total_forecast_volume_ton), 2)

    # 1. Fleet Requirements
    fleet = calculate_fleet_requirements(volume)
    rec_trucks = fleet["recommended_trucks"]

    # 2. Manpower Requirements
    manpower = calculate_manpower_requirements(rec_trucks)

    # 3. Dynamic Operational Efficiency
    efficiency = calculate_operational_efficiency_score(
        recommended_trucks=rec_trucks,
        forecast_volume_ton=volume,
        rainfall_mm=rainfall_mm,
        has_event=has_event,
        is_rush_hour=is_rush_hour
    )

    # 4. Collection Time based on Fleet Throughput
    collection = calculate_collection_time(
        forecast_volume_ton=volume,
        recommended_trucks=rec_trucks,
        rainfall_mm=rainfall_mm,
        has_event=has_event,
        is_rush_hour=is_rush_hour,
        operational_efficiency=efficiency["score_percent"] / 100.0
    )

    # 5. Forecast Reliability Score
    reliability = calculate_forecast_reliability_score(
        test_mape=test_mape,
        has_live_weather=has_live_weather,
        has_verified_bps=has_verified_bps,
        forecast_days=forecast_days
    )

    # Format human-friendly labels avoiding false precision
    loads_int = math.ceil(fleet["required_truck_loads"])

    return {
        "forecast_volume_ton": volume,
        # Backward-compatible fields
        "trucks_needed": rec_trucks,
        "manpower": manpower["total_personnel"],
        "estimated_duration_hours": collection["adjusted_hours"],
        "efficiency_rate": efficiency["display"],
        "required_truck_loads": fleet["required_truck_loads"],
        # Deep structured explainable modules
        "recommended_fleet": fleet,
        "manpower_breakdown": manpower,
        "collection_time": collection,
        "operational_factors": collection["factors"],
        "operational_efficiency": efficiency,
        "reliability": reliability,
        "ui_presentation": {
            "recommended_fleet_display": f"{rec_trucks} Trucks",
            "fleet_subtitle": f"{int(fleet['truck_capacity_ton'])} ton capacity / truck",
            "crew_display": f"{manpower['total_personnel']} Personnel",
            "crew_subtitle": f"{manpower['drivers']} drivers + {manpower['collectors']} collectors",
            "collection_time_display": f"{collection['adjusted_hours']} Hours",
            "collection_time_subtitle": "Adjusted for traffic, weather & events",
            "truck_loads_display": f"~{loads_int} Loads",
            "efficiency_display": efficiency["display"],
            "reliability_display": reliability["display"]
        },
        "calculation_method": "DETERMINISTIC_SIMULATION",
        "operational_assumptions": {
            "note": "Prototype Operational Assumptions — not validated against DLH specifications",
            "truck_capacity_ton": LOGISTICS_CONFIG["truck_capacity_ton"],
            "load_factor": LOGISTICS_CONFIG["load_factor"],
            "operational_buffer": LOGISTICS_CONFIG["operational_buffer"],
            "crew_per_truck": LOGISTICS_CONFIG["crew"]["crew_per_truck"],
            "collection_rate_ton_per_hour": LOGISTICS_CONFIG["collection_rate_ton_per_hour"]
        }
    }