Initial commit: T-ALNS-RRD paper reproduction project

- Paper: Optimizing urban last mile delivery efficiency (Liu & Wang, 2025)
- 5 algorithms: Static-VRPTW, TA-Greedy, ALNS-Base, T-ALNS, T-ALNS-RRD
- v1 baseline + v2 calibrated experiments with full results
- Tabu memory ablation study with convergence analysis
- Comprehensive final report (FINAL_REPORT.md)
This commit is contained in:
huangfu
2026-06-02 21:11:00 +08:00
commit 8ff9715fb7
45 changed files with 9293 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# Python
__pycache__/
*.pyc
*.pyo
*.egg-info/
# Generated data (large binary files)
t_alns_rrd_reproduction/data/synthetic/*.npy
t_alns_rrd_reproduction/data/synthetic/*.csv
t_alns_rrd_reproduction/data/synthetic/metadata.yaml
# Results (large binary, regeneratable)
t_alns_rrd_reproduction/results/**/*.png
t_alns_rrd_reproduction/results/**/*.npz
t_alns_rrd_reproduction/results/**/per_seed_costs.csv
# IDE
.vscode/
.idea/
*.swp
# OS
.DS_Store

View File

@@ -0,0 +1,10 @@
{
"sessionID": "ses_196b3c5ddffe1eBENDBW7mQepW",
"updatedAt": "2026-06-01T13:23:51.989Z",
"sources": {
"background-task": {
"state": "idle",
"updatedAt": "2026-06-01T13:23:51.989Z"
}
}
}

1696
paper.md Normal file

File diff suppressed because it is too large Load Diff

680
reproduction_plan.md Normal file
View File

@@ -0,0 +1,680 @@
# T-ALNS-RRD 论文复现完整计划
## 0. 文档概要
**论文标题**: Optimizing urban last mile delivery efficiency through dynamic vehicle routing heuristics and traffic flow analysis
**作者**: Lu Liu, Tianxia Wang (Yibin Vocational and Technical College)
**发表**: 2025, Scientific Reports
**复现类型**: 基于合成数据的算法机制复现 (Methodological Reproduction)
---
## 1. 复现目标体系
### 1.1 核心目标
复现论文提出的 T-ALNS-RRD 框架的核心算法链:
```
Static-VRPTW → TA-VRPTW-Greedy → ALNS-Base → T-ALNS → T-ALNS-RRD
```
### 1.2 验证目标 (5 个量化趋势)
| # | 趋势 | 预期方向 |
|---|------|---------|
| 1 | 交通感知成本降低拥堵暴露 | TA-Greedy CES < Static CES |
| 2 | ALNS 相比贪婪算法进一步降低总成本 | ALNS TotalCost < TA-Greedy |
| 3 | Tabu memory 减少重复搜索,提高解稳定性 | T-ALNS σ < ALNS σ |
| 4 | RRD 实时调度减少突发事件下的延误 | T-ALNS-RRD Delay < T-ALNS Delay |
| 5 | 各模块叠加后整体表现优于静态规划 | T-ALNS-RRD 全指标最优 |
### 1.3 成功标准
1. 生成可重复的合成数据集
2. 运行至少 4 个算法版本 (Static, TA-Greedy, ALNS, T-ALNS)
3. 输出统一评价指标
4. 展示逐模块改进趋势
5. **优先级**: 先完成 Static → TA-Greedy → ALNS → T-ALNS 四个版本
6. RRD 作为增强实验,可用简化交通事故案例展示
---
## 2. 数据集设计 (与论文 Table 2 对齐)
### 2.1 基本规模参数
| 参数 | 值 | 来源 |
|------|-----|------|
| 客户数 n | 47 | 论文 §4.2 |
| 仓库 | 1 (node 0) | 论文 §4.2 |
| 车辆数 m | 4 | 论文 §4.2 |
| 车辆容量 Q | 120 kg | 论文 §4.2 |
| 服务时间 s_i | 4 min/customer | 论文 §4.2 |
| 时间窗口 | 3 类 (morning/afternoon/evening) | 论文 §4.2 |
| 运营时间 | 6:00-18:00 (12 hours) | 论文 §4.3 |
| 时间段 H | 12 (1h each) | 论文 §4.3 |
| 区域范围 | 8 km × 10 km | 论文 §4.1 |
| 路网规模 | 48 nodes, 2256 arcs | 论文 Table 2 |
### 2.2 客户数据生成
每个客户属性:
```
customer_id, x (km), y (km), demand (kg), service_time (min),
earliest_time, latest_time, priority
```
生成规则:
- `x ∈ [0, 8] km`, `y ∈ [0, 10] km`
- `demand ∈ [3, 12] kg` (均匀或带簇偏差)
- `service_time = 4 min`
- 时间窗口分 3 类:
- Morning: 9:00-12:00
- Afternoon: 13:00-16:00
- Evening: 17:00-20:00
**空间分布**: 生成 3 个客户簇 (住宅区/商业区/办公区),增加路径优化意义
### 2.3 路网与弧数据
**初版方案**: 完全图 (complete graph),每对节点一条弧
每条弧属性:
```
from_node, to_node, distance (欧氏), base_travel_time,
road_type (arterial/collector/residential)
```
道路速度:
| Road Type | Speed |
|-----------|-------|
| Arterial | 45 km/h |
| Collector | 30 km/h |
| Residential | 20 km/h |
升级方案 (Phase 2+): 使用 networkx 生成网格路网模拟真实道路
### 2.4 时间依赖交通矩阵 (3 个张量)
为每个 `(i, j, h)` 组合计算,其中 `h ∈ {0, 1, ..., 11}`:
```python
travel_time[i, j, h] # 行驶时间
congestion[i, j, h] # 拥堵权重 γ ∈ [0, 1]
uncertainty[i, j, h] # 可靠性不确定性 η
```
**交通拥堵乘子** (论文 §3.2):
| 时间段 | 时段 | 拥堵乘子 |
|--------|------|---------|
| 6:00-7:00 | 早间 | 1.0 |
| 7:00-9:00 | 早高峰 | 1.6 |
| 9:00-11:00 | 上午 | 1.2 |
| 11:00-13:00 | 午间 | 1.0 |
| 13:00-16:00 | 午后 | 1.2 |
| 16:00-18:00 | 晚高峰 | 1.7 |
行驶时间计算:
```
t_ij^(h) = base_time_ij × traffic_multiplier_h × road_noise
```
拥堵权重 (论文 Eq.9):
```
ρ_ij(T_i) = θ × γ_ij^(h), if T_i ∈ τ_h
```
风险调整时间 (论文 Eq.10):
```
t'_ij^(h) = t_ij^(h) + β × η_ij^(h)
```
**需要满足 FIFO 一致性** (论文 Eq.11):
```
T_i^1 ≤ T_i^2 ⇒ T_i^1 + t_ij(T_i^1) ≤ T_i^2 + t_ij(T_i^2)
```
---
## 3. 问题建模与目标函数
### 3.1 DVRPTW-TA 形式化 (论文 §3.1)
在完全图 G=(N, A) 上建模N = {0, 1, ..., 47}A ⊆ N × N。
### 3.2 复合成本函数 (论文 Eq.1)
```
Cost = Σ_k Σ_(i,j) x_ijk [t_ij(T_i) + λ₂ρ_ij(T_i)] + λ₁ Σ_j δ_j
其中:
δ_j = max{0, S_j - l_j} - 迟到惩罚 (论文 Eq.2)
t_ij(T_i) - 时间依赖行驶时间
ρ_ij(T_i) - 拥堵惩罚
λ₁ - 迟到惩罚权重
λ₂ - 拥堵暴露权重
```
### 3.3 约束条件
1. **每个客户服务一次** (Eq.3): `Σ_k Σ_j x_ijk = 1, ∀i ∈ N\{0}`
2. **车辆容量** (Eq.4): `Σ_i d_i Σ_j x_ijk ≤ Q, ∀k`
3. **时间可行性** (Eq.5): `A_j ≥ T_i + t_ij(T_i), S_j ≥ max{A_j, e_j}`
4. **软时间窗** (Eq.6): `S_j ≤ l_j + ε_j, ε_j ≥ 0`
5. **子回路消除** (Eq.7): MTZ 约束
---
## 4. 算法实现详细设计
### 4.1 Baseline 1: Static-VRPTW
**特点**: 不考虑时间变化、不考虑拥堵惩罚
**方法**: Greedy insertion heuristic
**输入**: 固定 base_travel_time
**输出**: 车辆路径方案
```
Algorithm:
1. 所有车辆从 depot 出发
2. 对每个未分配客户,计算插入各车辆当前路径各位置的最小边际成本
3. 选择边际成本增量最小的 (customer, vehicle, position)
4. 满足容量和时间窗约束则插入
5. 重复直到所有客户分配完毕
```
### 4.2 Baseline 2: TA-VRPTW-Greedy
**特点**: 考虑 time-dependent travel time + congestion penalty但不使用 ALNS
**方法**: 与 Static 相同框架,但使用 t_ij(T_i) 和 ρ_ij(T_i)
**与 Static 的关键差异**: 边际成本计算使用 time-dependent travel time
### 4.3 Baseline 3: ALNS-Base (论文 §3.3.1 + Algorithm 1)
**核心组件**:
```
Destroy Operators (论文 §3.3.1):
- Random Removal: 均匀随机移除 α×n 个客户
- Worst Removal: 移除造成最大延迟/拥堵成本的客户
- Relatedness Removal: 移除地理或时间特征相似的客户
移除数量: |C| = ⌊α × n⌋, α ∈ [0.1, 0.4] (论文 Eq.15)
Repair Operators:
- Greedy Insertion: 最小化边际成本 (论文 Eq.16-17)
- Regret-2 Insertion: 最小化最优和次优插入位置差距
- Time-Window-Aware Insertion: 最小化时间窗违规
Acceptance: Simulated Annealing (论文 Eq.20-21)
P_accept = 1 if f(S') < f(S)
= exp(-(f(S') - f(S)) / τ_t) otherwise
τ_{t+1} = γ × τ_t, γ ∈ (0, 1)
Adaptive Operator Weights (论文 Eq.18-19):
p_h(t) = ω_h(t) / Σ ω_j(t)
ω_h(t+1) = (1-ξ) ω_h(t) + ξ × θ_r
奖励等级:
σ₁: 找到全局最优解
σ₂: 改善当前解
σ₃: 被接受但未改善
σ₄: 被拒绝
终止条件:
max_iter = 1000 (论文 §4.8)
time_limit = 600s
stall: 连续 T_stall 代无改进
```
**插入成本计算** (论文 Eq.16-17):
```
Δf_ijk(T_j) = t_ji(T_j) + s_i + t_ik(T_j + t_ji + s_i) - t_jk(T_j)
+ λ₁ × δ_i(T_i) + λ₂ × ρ_ji(T_j)
ΔF_jik = Δf_jik^local + Σ_(u,v)∈Asuffix [t_uv(T') + λ₁δ_v + λ₂ρ_uv]
- Σ_(u,v)∈Asuffix_old [t_uv + λ₁δ_v + λ₂ρ_uv]
```
### 4.4 Baseline 4: T-ALNS (论文 §3.3.2 + Algorithm 2)
在 ALNS 基础上增加三层 Tabu Memory:
#### Move-Based Tabu (论文 Eq.22-23)
```
存储: (C_removed, h_d, h_r, t_iter) 元组
判定: 如果存在记录 (Ĉ, ĥ_d, ĥ_r, ť) 满足:
|C_removed ∩ Ĉ| ≥ μ × min(|C_removed|, |Ĉ|)
AND t_current - ť ≤ τ_move
则为 Tabu
自适应 tenure (论文 Eq.32):
τ_move(t+1) = min(τ_move+1, τ_max) if no improvement in τ_stall
= max(τ_move-1, τ_min) if improvement found
```
#### Solution-Based Tabu (论文 Eq.23-24)
```
哈希函数: H(S) = Σ_k Σ_i φ(v_ki, v_k(i+1)) mod P
存储: H(S') 作为环形缓冲区 (最大 1000 或 2^⌈log₂(n)⌉)
Tabu: 如果 H(S') ∈ T_sol 且在最近 τ_sol 代内记录
```
#### Frequency Memory (论文 Eq.24-26)
```
客户-车辆分配频率: F^cv_ik(t+1) = F^cv_ik(t) + 1_[i∈R_k(S)]
时间位置频率: F^tp_ij(t+1) = F^tp_ij(t) + Σ_k 1_[v_kj = i]
拥堵加权 (Eq.33):
F^cv_ik(t+1) = F^cv_ik(t) + 1_[i∈R_k] × (1 + Σ_j ρ_ji(T_j))
定期归一化 (Eq.32): F ← ⌊F/κ⌋ 每 ν
```
#### 多样化强度控制 (论文 Eq.26-28)
```
δ(t) = ω₁ × (t - t_last_best)/T_max
+ ω₂ × |T_move|/|T_move|_max
+ ω₃ × σ(F^cv)
if δ(t) > δ_max: 切换到多样化模式
p'_h(t) = η × p_h(t) + (1-η) × div_h(t)/Σ div_j(t)
```
#### 赦免准则 (论文 Eq.29-31)
1. **全局最优赦免**: f(S') < f(S*)
2. **低频赦免**: min F^cv_ik < β × F̄^cv (鼓励未用分配)
3. **交通适应赦免**: 拥堵成本显著降低
### 4.5 Proposed: T-ALNS-RRD (论文 §3.3.3 + Algorithm 3)
#### 事件检测与分类
```
事件类型 (论文 §3.3.3):
E1: 关键交通事故 (道路封闭/事故/严重拥堵)
E2: 紧急订单插入 (高优先级、紧时间窗)
E3: 容量违规 (需求波动超过车辆容量)
E4: 服务时间违规 (累计延迟威胁时间窗)
紧急度评分 (论文 Eq.35):
Ψ(e,t) = α_e × (t_deadline - t_current)/t_horizon
+ β_e × impact(e) + γ_e × cost_increase(e)
```
#### 候选动作空间
```
E1 - 局部改道: K_s=5 条 FIFO 一致的 k-shortest-time 绕行
预算 Δ_max=8 min
E1 - 客户重分配: 3km 范围内、≤10min ETA 差的车辆
每车 top-2 regret 位置
E2 - 紧急插入: top-r 插入位置 (Eq.16)
延迟变体: ≤30min 偏移,惩罚 λ₁δ_j
全部不可行则 subcontract
E3 - 负载重分配: 最小可行子集转移,最多 3 次
E4 - 加速/放宽: 局部 2-opt 重排
不可行则临时容差 ϵ_tolerance
最大动作数 |A_e| ≤ 20
```
#### Rollout 仿真 (论文 Eq.36-39)
```
价值函数 (Eq.36):
V^rollout(s,a,H) = E[ Σ_h Σ_k Σ_(i,j)∈A^h_k
[t_ij + λ₂ρ_ij] + λ₁ Σ_j δ_j(S^h_j) ]
交通演化 (Eq.38):
t^rollout_ij(T_i + s) = t^(r)_ij + s/Δt × (t^(r+1)_ij - t^(r)_ij)
Tabu 调整价值 (Eq.39):
V^adjusted = V^rollout - τ_penalty × 1_[Tabu(a)]
+ τ_bonus × Diversification(a)
复合决策分数 (Eq.40):
Σ(a,e,s) = ω₁ × V^adjusted + ω₂ × Stability(a,s) + ω₃ × Recovery(a,s)
稳定性 (Eq.41):
Stability(a,s) = Σ_k |R^a_k ∩ R^s_k| / |R^a_k R^s_k|
恢复性 (Eq.42):
Recovery(a,s) = -Σ_k Σ_i |OptimalPosition(i) - CurrentPosition(i,a)|
```
#### 自适应资源分配 (论文 Eq.43-44)
```
H_rollout = max(H_min, H_max - α_urgency × Ψ(e,t))
N_sim = max(N_min, ⌊(T_available - T_overhead) / T_sim⌋)
```
---
## 5. 评价指标体系 (论文 §4.4)
### 5.1 主要指标
| 指标 | 定义 | 公式 |
|------|------|------|
| Total Cost | 综合运营成本 | Eq.1 |
| Travel Time Cost | 总行驶时间成本 | Σ t_ij(T_i) |
| Delay Penalty | 迟到惩罚 | λ₁ Σ δ_j |
| Congestion Cost | 拥堵成本 | Σ ρ_ij(T_i) |
| OTDR | 准时送达率 | N_ontime / (n-1) (Eq.45) |
| Average Delay | 平均迟到时间 | Σ δ_j / n_late |
| Max Delay | 最大迟到时间 | max δ_j |
| Late Customers | 迟到客户数 | count(S_j > l_j) |
| CES | 拥堵暴露分数 | Σ ρ_ij(T_i) (Eq.47) |
| Average Route Duration | 平均路线时长 | (1/m) Σ (T_k^end - T_k^start) (Eq.46) |
### 5.2 算法性能指标
| 指标 | 定义 |
|------|------|
| Computation Time | 算法运行时间 |
| Iterations to Best | 找到最优解的迭代次数 |
| Convergence Rate | 收敛速度 (improvements/iteration) |
| Solution Stability (σ) | 解的稳定性 (标准差) |
### 5.3 RRD 专属指标
| 指标 | 定义 |
|------|------|
| Event Response Rate | 事件响应成功率 |
| Avg Response Time | 平均响应时间 (ms) |
| Cost Reduction via RRD | RRD 带来的成本降低 |
| Adaptation Success Rate | 自适应成功率 |
---
## 6. 实验设计 (论文 §5)
### 6.1 实验一: 主对比实验 (对应论文 Table 5-7, Fig 8-10)
**算法**: Static / TA-Greedy / ALNS-Base / T-ALNS / T-ALNS-RRD
**配置**:
- 30 个随机种子 (论文 §5.1: seeds 1-30)
- max_iter = 1000
- time_limit = 600s (可缩短)
- 实验结果求均值 ± 标准差
**输出表格**:
```
Algorithm | TotalCost | TravelTime | DelayPenalty | Congestion | OTDR | CES | CompTime
```
### 6.2 实验二: 消融实验 (对应论文 Table 11, Fig 13)
**配置**:
```
Base ALNS
+ Move Tabu Only
+ Solution Tabu Only
+ Frequency Memory Only
Full T-ALNS
+ Traffic Events Only
+ Urgent Delivery Only
+ All RRD Events (Full T-ALNS-RRD)
```
### 6.3 实验三: 交通不确定性鲁棒性实验 (对应论文 Table 14, Fig 15)
**配置**:
```
σ ∈ {0.1, 0.2, 0.3, 0.5}
每组重复 30 次
```
**输出指标**: 各 σ 下的 TotalCost、OTDR、CES、Robustness Index
### 6.4 实验四: 参数敏感性实验 (对应论文 Table 3)
**维度**:
| 参数 | 取值 |
|------|------|
| 客户数 | 30, 40, 47, 60 |
| 车辆数 | 2, 3, 4, 5, 6 |
| 车辆容量 (kg) | 80, 100, 120, 140, 160 |
### 6.5 实验五: 实时事件响应实验 (对应论文 Table 8, Fig 11)
**事件类型** (最低实现 3 类):
```
1. Traffic incident: 某边 travel_time 临时 × 2-3
2. Urgent order: 中途新增一个客户
3. Time-window risk: 预测某客户即将迟到
```
**输出**: Event Type | Frequency | Success Rate | Cost Reduction | Response Time
### 6.6 收敛性分析 (对应论文 Table 9-10, Fig 12)
记录 epoch-level 的 cost 下降:
```
Epoch 5, 10, 15, ..., 50
绘制 ALNS-Base vs T-ALNS vs T-ALNS-RRD 收敛曲线
```
---
## 7. 可视化要求 (论文 Fig 7-16)
| 图编号 | 内容 | 类型 |
|--------|------|------|
| Fig 1 | 路线图: 仓库、47 客户、4 条路径 | 空间散点 + 连线 |
| Fig 2 | 算法框架图: ALNS → Tabu → RRD | 流程图 |
| Fig 3 | 主实验: Total Cost / OTDR / CES 柱状图 | 分组柱状图 |
| Fig 4 | 收敛曲线: ALNS vs T-ALNS vs T-ALNS-RRD | 折线图 |
| Fig 5 | 消融实验: 各模块贡献瀑布图 | 柱状图 |
| Fig 6 | RRD 示例: 扰动前后路线变化对比 | 双面板空间图 |
---
## 8. 项目代码结构
```
t_alns_rrd_reproduction/
├── README.md
├── requirements.txt # numpy, pandas, matplotlib, networkx, pyyaml, tqdm
├── configs/
│ ├── default.yaml # 全局默认参数
│ ├── experiment_main.yaml # 主对比实验配置
│ ├── experiment_ablation.yaml # 消融实验配置
│ └── experiment_robustness.yaml
├── src/
│ ├── __init__.py
│ ├── data_generator.py # 合成数据集生成
│ ├── problem.py # DVRPTW-TA 问题定义、Solution 数据结构
│ ├── cost.py # 成本函数计算 (Eq.1, Eq.16-17)
│ ├── schedule.py # 调度评估 (时间传播)
│ ├── baselines/
│ │ ├── __init__.py
│ │ ├── static_vrptw.py # Baseline 1: Static-VRPTW greedy
│ │ └── ta_greedy.py # Baseline 2: TA-VRPTW-Greedy
│ ├── alns/
│ │ ├── __init__.py
│ │ ├── operators_destroy.py # Random/Worst/Relatedness Removal
│ │ ├── operators_repair.py # Greedy/Regret-2/Time-Window-Aware Insertion
│ │ ├── acceptance.py # Simulated Annealing
│ │ └── alns_base.py # ALNS-Base (Algorithm 1)
│ ├── tabu/
│ │ ├── __init__.py
│ │ ├── move_tabu.py # Move-based Tabu list (Eq.22-23)
│ │ ├── solution_tabu.py # Solution hash memory (Eq.24)
│ │ ├── frequency_memory.py # Frequency matrices (Eq.25-26)
│ │ └── t_alns.py # T-ALNS (Algorithm 2)
│ ├── rrd/
│ │ ├── __init__.py
│ │ ├── event_generator.py # 事件检测与分类 (Eq.35)
│ │ ├── candidate_actions.py # 候选动作生成
│ │ ├── rollout.py # Rollout 仿真 (Eq.36-39)
│ │ ├── dispatch.py # 调度决策 (Eq.40-42)
│ │ └── t_alns_rrd.py # T-ALNS-RRD (Algorithm 3)
│ ├── experiments/
│ │ ├── run_main_comparison.py
│ │ ├── run_ablation.py
│ │ ├── run_robustness.py
│ │ ├── run_sensitivity.py
│ │ └── run_convergence.py
│ └── visualization/
│ ├── plot_routes.py # 路线地图
│ ├── plot_convergence.py # 收敛曲线
│ ├── plot_results.py # 实验结果柱状图
│ └── plot_rrd.py # RRD 前后对比图
├── data/
│ └── synthetic/ # 生成的合成数据
├── results/
│ ├── tables/
│ ├── figures/
│ └── logs/
└── tests/ # 可选: 关键逻辑单元测试
```
---
## 9. 分阶段实施计划
### 阶段 1: 基础可运行 (预计 2-3 天)
**目标**: 数据生成 + 两个 baseline 可运行
```
实现内容:
1. data_generator.py # 生成客户、路网、交通矩阵
2. problem.py # Solution 数据结构、约束检查
3. cost.py # Eq.1 成本计算
4. schedule.py # 时间传播
5. static_vrptw.py # Static-VRPTW greedy
6. ta_greedy.py # TA-VRPTW-Greedy
验收标准:
- 能生成 47 客户、4 车、12 时段的完整数据集
- Static-VRPTW 和 TA-Greedy 能输出可行路径
- 能计算 Total Cost, OTDR, CES
- 两个算法的 CES 差异体现交通感知效果
```
### 阶段 2: 核心算法复现 (预计 3-4 天)
**目标**: ALNS + T-ALNS 可运行,产消融表、收敛图
```
实现内容:
1. operators_destroy.py # 3 种 destroy 算子
2. operators_repair.py # 3 种 repair 算子 (含 Eq.16-17)
3. acceptance.py # SA 接受准则 (Eq.20-21)
4. alns_base.py # ALNS 主循环 (Algorithm 1)
5. move_tabu.py # 移动 Tabu (Eq.22-23, 32)
6. solution_tabu.py # 解 Tabu (Eq.24)
7. frequency_memory.py # 频率记忆 (Eq.25-26, 33)
8. t_alns.py # T-ALNS (Algorithm 2)
验收标准:
- ALNS-Base 能运行 1000 代并输出最优解
- T-ALNS 相比 ALNS-Base 有改善 (Total Cost ↓, OTDR ↑)
- 不同 Tabu 模块可独立开关进行消融
- 收敛曲线显示 T-ALNS 更快收敛
```
### 阶段 3: 实时调度增强 (预计 2-3 天)
**目标**: T-ALNS-RRD 可运行,产事件响应表
```
实现内容:
1. event_generator.py # 4 种事件检测 (Eq.35)
2. candidate_actions.py # 候选动作枚举 (含验证)
3. rollout.py # Rollout 仿真 (Eq.36-39)
4. dispatch.py # 复合决策 (Eq.40-42)
5. t_alns_rrd.py # T-ALNS-RRD (Algorithm 3)
验收标准:
- 能模拟至少 Traffic Incident 和 Urgent Order
- Rollout 能在 120s 内返回调度决策
- 调度后 OTDR 相比不调度有所提升
- 路线变化可视化 (before/after 对比)
```
### 阶段 4: 实验与可视化 (预计 2 天)
**目标**: 跑完全部实验,产出论文风格图表
```
实现内容:
1. run_main_comparison.py
2. run_ablation.py
3. run_robustness.py
4. run_sensitivity.py
5. 6 张可视化图
验收标准:
- 5 个实验全部可执行
- 产出至少 5 张可放入 PPT 的图表
- 表格数据保存在 results/tables/
- 图保存在 results/figures/
```
---
## 10. 技术栈与依赖
```
Python 3.11
numpy - 矩阵运算、随机数
pandas - 数据管理、表格输出
matplotlib - 可视化
networkx - 路网建模 (Phase 2+ 可选)
pyyaml - 配置文件
tqdm - 进度条
```
---
## 11. 关键参数默认值 (论文参数)
| 参数 | 默认值 | 来源 |
|------|--------|------|
| λ₁ (lateness) | 1.0 | 论文 Eq.1 |
| λ₂ (congestion) | 1.0 | 论文 Eq.1 |
| θ (congestion scale) | 1.0 | 论文 Eq.9 |
| β (risk aversion) | 0.3 | 论文 Eq.10 |
| α (destroy ratio) | [0.1, 0.4] | 论文 Eq.15 |
| τ₀ (initial temp) | 0.05 × Z(S⁰) | Algorithm 1 |
| γ (cooling rate) | 0.99975 | Algorithm 1 |
| ξ (reaction factor) | 0.1 | 论文 Eq.19 |
| τ_move (move tenure) | 7 | 论文 Table 4 |
| τ_sol (solution tenure) | 15 | 论文 Table 4 |
| μ (overlap threshold) | 0.5 | 论文 Eq.23 |
| δ_max (diversification) | 0.7 | 论文 Eq.27 |
| H_rollout | 30-120 min | 论文 Eq.43 |
| N_sim | 2-50 | 论文 Eq.44 |
| ω₁, ω₂, ω₃ (composite) | 0.4, 0.3, 0.3 | 论文 Eq.40 |
---
## 12. 风险与应对
| 风险 | 应对 |
|------|------|
| 完全图计算量过大 (47² = 2209 arcs × 12h × 3 params) | 完全图可行,约 81000 条数据numpy 可处理 |
| ALNS 收敛慢 | 放宽 stall 条件,适当增加 max_iter |
| T-ALNS Tabu 逻辑复杂 | 先实现 Move Tabu Only验证效果再加其他 |
| RRD rollout 耗时 | 降低 H_rollout 到 30min减少 N_sim 到 10 |
| 结果与论文差异大 | 这是合成数据复现,重点是趋势验证而非精确复刻 |
---
## 13. 复现声明模板
最终汇报时使用如下定位:
> 由于原文数据集需向作者合理请求,且目前尚未获得完整数据与代码,本项目采用与论文实验规模和数据结构相近的自定义合成数据集,复现其核心算法流程和对比实验框架。复现重点在于验证不同算法模块对总成本、准时率、拥堵暴露和实时扰动响应能力的相对影响,而非逐项复刻原文数值结果。本项目属于基于合成数据的算法机制复现 (methodological reproduction)。
---
## 14. 预估时间线
| 阶段 | 内容 | 预估时间 |
|------|------|---------|
| 阶段 1 | 数据集 + 2 个 baseline | 2-3 天 |
| 阶段 2 | ALNS + T-ALNS | 3-4 天 |
| 阶段 3 | RRD 实时调度 | 2-3 天 |
| 阶段 4 | 实验跑全 + 可视化 | 2 天 |
| **总计** | | **9-12 天** |

View File

@@ -0,0 +1,393 @@
# T-ALNS-RRD 论文复现——期末详细汇报
---
## 第一章:论文理解
### 1.1 问题建模DVRPTW-TA
论文将城市末端配送建模为 **带时间窗和交通感知的动态车辆路径问题 (DVRPTW-TA)**,在有向图 G=(N,A) 上求解。N={0,1,...,n},节点 0 是仓库,其余为客户。
**问题要素**
- 客户 i需求 d_i(kg)、服务时间 s_i(min)、时间窗 [e_i, l_i](最早/最晚开始服务时间)
- 车辆集 K={1,...,m},容量 Q
- 弧 (i,j) 的行驶时间 t_ij(T_i) 取决于从 i 的出发时刻 T_i
- ρ_ij(T_i) 是拥堵惩罚,衡量该弧在出发时刻的拥堵强度
**核心目标函数 (Eq.1)**
$$\min \sum_{k \in K} \sum_{(i,j) \in A} x_{ijk} \left[t_{ij}(T_i) + \lambda_2 \rho_{ij}(T_i)\right] + \lambda_1 \sum_{j \in N \setminus \{0\}} \delta_j$$
拆解这个公式:
- **行驶时间项** t_ij(T_i)车辆在路上花费的时间。时间依赖意味着同一段路8:00 出发和 12:00 出发用时不同
- **拥堵暴露项** λ₂·ρ_ij(T_i):λ₂ 是拥堵权重系数ρ_ij(T_i) 是该弧在 T_i 时刻的拥堵惩罚值。拥堵越严重,惩罚越大。λ₂=1.0 意味着 "多堵 1 分钟的代价" 和 "多开 1 分钟" 相同
- **迟到惩罚项** λ₁·Σ δ_jδ_j = max{0, S_j - l_j} (Eq.2)。如果服务开始时间 S_j 晚于客户要求的最晚时间 l_j差额即为 δ_j。λ₁=1.0 赋予迟到与行驶时间相同的权重
**时间传播规则 (Eq.5)**
$$A_j = T_i + t_{ij}(T_i),\quad S_j = \max\{A_j, e_j\},\quad T_j = S_j + s_j$$
- 到达时间 A_j = 前一点出发时间 + 当前弧的行驶时间
- 如果到早了A_j < e_j等待到 e_j 再开始服务
- 服务完 s_j 分钟后出发去下一站
**约束条件 (Eq.3-7)**每个客户恰好被服务一次、车辆不超载、时间窗软约束允许迟到但惩罚、MTZ 子回路消除。
---
### 1.2 交通数据集成 (Eq.8-11)
运营时间 6:00-18:00 被离散为 H=12 个一小时区间 {τ_1, ..., τ_12}。
**分段常数行驶时间 (Eq.8)**
$$t_{ij}(T_i) = t_{ij}^{(h)},\quad \text{if } T_i \in \tau_h$$
即出发时间落在第 h 个区间,就使用该区间的行驶时间 t_ij^(h)。
**拥堵惩罚 (Eq.9)**
$$\rho_{ij}(T_i) = \theta \cdot \gamma_{ij}^{(h)},\quad \text{if } T_i \in \tau_h$$
γ_ij^(h)∈[0,1] 是归一化拥堵密度。论文原文 θ 未明确取值;本复现经校准取 θ=50使 ρ 的量级与行驶时间可比。
**风险调整时间 (Eq.10)**t'_ij = t_ij + β·η_ij。在确定型行驶时间上叠加可靠性边际 η,β=0.3 控制风险规避程度。实践中这相当于 "预留 buffer time"。
**FIFO 一致性 (Eq.11)**:晚出发不会早到达——这是交通流的基本物理约束,分段常数模型自动满足。
---
### 1.3 ALNS自适应大邻域搜索
ALNS 的核心是 **Destroy-Repair 循环**:每次迭代破坏当前解的一部分,再用另一种策略修复,通过反复试探找到更好的解。
**Destroy 算子 (3 种)**
- **Random Removal**:随机移除 α×n 个客户(α∈[0.1, 0.4]Eq.15
- **Worst Removal**:移除造成最大延迟或拥堵成本的客户
- **Relatedness Removal (Shaw Removal)**:先随机选一个 "种子" 客户,然后迭代移除与已移除客户最 "相关" 的客户。相关性 = 地理距离 + 时间窗相似度
**Repair 算子 (3 种)**
- **Greedy Insertion**:每次插入边际成本最低的客户
- **Regret-2 Insertion**:优先插入 "最优位置和次优位置差距最大" 的客户——差得越多,越应该趁早插入,否则越来越差
- **Time-Window-Aware**:优先插入时间窗最紧的客户
**插入成本 (Eq.16-17)**:以 Eq.16 计算局部边际成本(新弧行驶时间 - 旧弧行驶时间 + 迟到惩罚 + 拥堵惩罚Eq.17 进一步考虑插入后下游所有节点的到达时间因 FIFO 效应而推迟的影响suffix propagation
**自适应算子权重 (Eq.18-19)**:每个算子的选择概率 p_h(t) = ω_h(t) / Σ ω_j(t)。每 π=100 代更新权重:
$$\omega_h(t+1) = (1-\xi) \cdot \omega_h(t) + \xi \cdot \theta_r$$
ξ=0.1 控制对近期表现的敏感度。θ_r 分四级:新全局最优 σ₁=1.0 > 改善当前解 σ₂=0.5 > 被接受但未改善 σ₃=0.2 > 被拒绝 σ₄=0.0。
**模拟退火接受准则 (Eq.20-21)**
$$P_{accept} = \begin{cases} 1 & \text{if } f(S') < f(S) \\ \exp(-\frac{f(S')-f(S)}{\tau_t}) & \text{otherwise} \end{cases}$$
温度按几何冷却τ_{t+1} = γ × τ_tγ=0.99975。初始温度 T₀ = 0.05 × Z(S⁰)(初始解成本的 5%)。越往后接受劣解的概率越低——搜索从 "广泛探索" 逐渐收敛到 "精细优化"。
---
### 1.4 T-ALNS三层 Tabu 记忆
ALNS 的核心弱点是**搜索容易循环**——在局部最优附近反复兜圈浪费计算资源。T-ALNS 通过三层记忆结构阻止这种行为。
**Move Tabu (Eq.23)**:记录近期做过的 destroy-repair 操作 (C_removed, h_d, h_r, t)。如果新候选操作的被移除客户集合与某条记录的重叠超过 μ=50%,且在 tenure=7 代内,则宣布为 Tabu。tenure 自适应调整 (Eq.32):长时间无改善就延长(最多 12 代),有改善就缩短(最少 3 代)。
**Solution Tabu (Eq.24)**用多项式哈希对完整路线结构编码。H(S) = Σ_k Σ_i φ(v_ki, v_k(i+1)) mod P。如果候选解的哈希值在 τ_sol=15 代内出现过,则禁止——防止 "回到之前探索过的解"。
**Frequency Memory (Eq.25-26)**
- F^cv[i,k]:客户 i 被分配给车辆 k 的累积频率
- F^tp[i,j]:客户 i 在路线中出现位置 j 的频率
ν=50 代归一化(除以 κ=2防止溢出。
频率低的组合在搜索中被优先尝试,引导搜索探索未访问的配置空间。
**多样化强度控制 (Eq.27-28)**
$$\delta(t) = \omega_1 \cdot \frac{t - t_{last\_best}}{T_{max}} + \omega_2 \cdot \frac{|T_{move}|}{|T_{move}|_{max}} + \omega_3 \cdot \sigma(F^{cv})$$
当 δ(t) > δ_max=0.7 时,算子选择从纯性能导向切换到 "性能+多样化" 混合,鼓励探索新区域。
**赦免准则 (Eq.29-31)**:即使一个移动被 Tabu 禁止,以下三种情况仍可接受:
1. **全局最优赦免**:比已知最好解更好
2. **低频赦免**:目标客户-车辆组合很少被尝试F^cv_ik < β · F̄^cv
3. **交通适应赦免**:拥堵成本显著降低(< γ × 当前拥堵成本)
---
### 1.5 T-ALNS-RRDRollout 实时调度
在 T-ALNS 的长期优化基础上,增加实时调度层处理突发事件。
**四类事件 (E1-E4)**:交通事故、紧急加单、容量违规、时间窗风险。
**紧急度评分 (Eq.35)**
$$\Psi(e,t) = \alpha_e \cdot \frac{t_{deadline} - t_{current}}{t_{horizon}} + \beta_e \cdot \text{impact}(e) + \gamma_e \cdot \text{cost\_increase}(e)$$
三类事件各自的权重系数 (α,β,γ) 不同:交通事故侧重影响范围 (β=0.3),紧急加单和时窗风险侧重时间紧迫度 (α=0.7-0.8)。
**Rollout 仿真 (Eq.36)**:对候选动作在有限视界 H30-120min内进行蒙特卡洛仿真估算期望成本。交通演化使用分段线性外推 (Eq.38)。
**Tabu 调整 (Eq.39)**V_adjusted = V_rollout - τ_penalty × 1[Tabu] + τ_bonus × Diversification。如果在 Tabu 记忆中,扣 50 分;如果促进多样化,加 25 分。
**复合决策 (Eq.40-42)**
$$\Sigma(a,e,s) = 0.4 \cdot V^{adjusted} + 0.3 \cdot Stability + 0.3 \cdot Recovery$$
- Stability (Eq.41):路线结构变化越小越好——用 Jaccard 相似度 |R_a ∩ R_s| / |R_a R_s| 衡量
- Recovery (Eq.42):调整后位置与 T-ALNS 最优位置的偏差越小越好
---
## 第二章:复现方法详解
### 2.1 复现定位
由于论文数据集需向作者合理请求且暂未公开,本复现属于 **基于自定义合成数据的算法机制复现 (methodological reproduction)**。重点验证各模块的**相对贡献趋势**,而非精确复刻论文的绝对数值。
> 论文 §4.1 明确指出实验数据也是合成的_"A synthetic traffic-aware urban delivery scenario..."_ 论文使用 OSM 提取的上海路网拓扑,但客户点和配送场景是人工生成的。因此使用合成数据复现完全符合论文的实验逻辑。
### 2.2 数据集构造
模拟一个 8×10 km² 的城市配送区域。
**参数对照表**
| 参数 | 论文值 | 复现 v1 | 复现 v2 | 对齐说明 |
|------|:------:|:------:|:------:|---------|
| 客户数 | 47 | 47 | 55 | v1 对齐v2 增难度 |
| 车辆/容量 | 4/120kg | 4/120kg | 4/115kg | v2 收紧 |
| 需求范围 | 3-12 kg | 3-12 kg | 3-13 kg | 基本对齐 |
| 服务时间 | 4 min | 4 min | 4 min | 完全对齐 |
| 时间窗类别 | 三类 (9-12/13-16/17-20) | 同论文 | 同论文 | 完全对齐 |
| 窗口宽度 | 论文未明确 | 60-150 min | 30-90 min | v2 收紧 |
| 运营时段 | 6:00-18:00, H=12 | 同论文 | 同论文 | 完全对齐 |
| 道路速度 | 未明确具体值 | 45/30/20 km/h | 同 v1 | 合理取值 |
| 弧数 | 2256 | 2256 / 3080 | 3080 | 完全图 n(n-1) |
**客户空间分布**:论文 §4.2 描述为 _"spatial density map derived from historical commercial activity data, ensuring realistic clustering patterns in residential and commercial zones"_。复现生成了 3 个簇中心(住宅区/商业区/办公区),客户在簇中心周围以高斯分布 (σ=0.8km) 随机生成。
**交通矩阵生成**
拥堵乘子(论文 §3.2 对应):
| 区间 | 时段 | 乘子 | 含义 |
|:----:|------|:---:|------|
| 0-1 | 6:00-8:00 | 1.0 | 畅通 |
| 2-3 | 8:00-10:00 | 1.6 | 早高峰 |
| 4-5 | 10:00-12:00 | 1.2 | 回落 |
| 6-7 | 12:00-14:00 | 1.0 | 午间 |
| 8-9 | 14:00-16:00 | 1.2 | 午后 |
| 10-11 | 16:00-18:00 | 1.7 | 晚高峰 |
拥堵权重 γ 通过式 γ = min(1, max(0, (multiplier-0.9)/0.9)) 将乘子映射到 [0,1]:乘子 1.0→γ≈0.11,乘子 1.7→γ≈0.89。
**v1→v2 的关键校准——拥堵惩罚 ρ 的重定义**
v1ρ = θ × γ,θ=1.0 → ρ∈[0,1] → CES ≈ 25。论文 CES ~1300-2850 → v1 差 60-100 倍。
v2ρ = θ × base_time × max(0, multiplier-1) × γ,θ=50。此时 ρ 反映 "拥堵造成的实际额外时间成本(分钟量级)"CES 进入 864-3194 范围,与论文可比。
**路网类型分配**:仓库连接的主干道概率 (动脉 50%/次干 35%/支路 15%),客户间短距离弧的支路概率更高 (60% vs 10%)——模拟了 "主干道快但远,支路慢但近" 的真实路网特征。
### 2.3 算法实现:论文公式→代码对照
#### Static-VRPTW (Baseline 1)
**论文描述**_"Employs a static greedy insertion heuristic...assuming all travel costs are symmetric and fixed."_
**实现**:贪婪插入,按客户最早时间窗排序。构建路径时使用 12 时段平均行驶时间(而非仅 interval 0评估时与其他算法使用相同 Eq.1 成本函数。关键区别:优化时不考虑时间依赖和拥堵,但评估时面对同样的时变环境——反映 "不考虑交通的静态规划在真实路况下的表现"。
#### TA-VRPTW-Greedy (Baseline 2)
**论文描述**_"Integrates time-dependent travel times and congestion penalties, but utilizes a non-iterative greedy insertion strategy."_
**实现**:使用 Eq.16-17 完整插入成本(含 t_ij(T_i)、λ₁δ_i、λ₂ρ_ji但不迭代——这是 "有交通感知但没有搜索" 的基线。
#### ALNS-Base (Baseline 3, Algorithm 1)
| 伪代码行 | 代码文件:行 | 实现方式 |
|---------|-----------|---------|
| S⁰ via greedy | alns_base.py:_construct_initial | 同 TA-Greedy 贪心 |
| ω_h←1.0 | alns_base.py:solve | 初始权重全 1 |
| T₀←0.05×Z(S⁰) | alns_base.py:solve | SA 初温=成本 5% |
| SelectOperator roulette | alns_base.py:_select_operator | 按 Eq.18 轮盘赌 |
| Destroy-Repair (Eq.16-17) | operators_destroy.py, operators_repair.py | 3×3 算子组合 |
| EvaluateCost traffic-aware | cost.py:compute_total_cost | Eq.1 完整 |
| SA accept (Eq.20-21) | acceptance.py:accept | P=exp(-Δf/τ) |
| Update weights (Eq.19) | alns_base.py:_update_weights | 每 100 代 |
#### T-ALNS (Baseline 4, Algorithm 2)
**Move Tabu (Eq.23)**`tabu/move_tabu.py`
- 存储 (frozenset(C_removed), d_op, r_op, iter) 四元组
- 交集比例 ≥ μ=0.5 且在 tenure 内 → Tabu
- 自适应 tenure (Eq.32):无改善增到 12有改善降到 3
**Solution Tabu (Eq.24)**`tabu/solution_tabu.py`
- 多项式哈希 H(S) = Σ_k Σ_i (a×1000+b)×31^pos mod 10^9+7
- 环形缓冲区,容量 1000
**Frequency Memory (Eq.25-26)**`tabu/frequency_memory.py`
- F^cv[n+1, m] 和 F^tp[n+1, n+1] 矩阵
-ν=50 代归一化(除以 κ=2
**赦免准则 (Eq.29-31)**`tabu/t_alns.py:_check_aspiration()`
- 全局最优赦免、低频赦免 (β=0.3)、交通适应赦免 (γ=0.8)
#### T-ALNS-RRD (Proposed, Algorithm 3)
| 组件 | 论文 | 代码 |
|------|------|------|
| 事件检测 | Eq.35 | event_generator.py |
| 候选动作 | | candidate_actions.py |
| Rollout 仿真 | Eq.36-38 | rollout.py |
| Tabu 调整 | Eq.39 | rollout.py:evaluate_action |
| 复合决策 | Eq.40-42 | dispatch.py |
---
## 第三章:评价指标详解
### 3.1 Total Cost总成本
**定义 (Eq.1)**Cost = TravelTime + λ₁·LatePenalty + λ₂·CongestionPenalty
**含义**:算法的直接优化目标。λ₁=λ₂=1.0,即三者的单位成本等价。
**各算法如何影响这个指标**
- Static没有交通感知 → 路线在高峰期困入拥堵 → 三项全高
- TA-Greedy知道拥堵分布 → 避开高峰期路段 → 拥堵惩罚大幅降低
- ALNS全局搜索 → 找到更优的客户-车辆-位置组合 → 行驶时间+迟到+拥堵同时下降
- T-ALNS记忆防循环 → 不用在局部最优浪费迭代 → 收敛到更低成本
- RRD事故后重新规划 → 避免事故导致的连锁延误
### 3.2 OTDR准时送达率
**定义 (Eq.45)**OTDR = 准时送达客户数 / 总客户数。S_j ≤ l_j 视为准时。
**含义**:这不是成本指标,而是**服务质量指标**。低成本但一半客户迟到,在实际运营中不可接受。
**各算法行为差异及原因**
- Static (43.6%):路线用平均时间规划,实际交通中大量时间窗被错过
- TA-Greedy (91.3%):知道堵车时间 → 合理安排 → 大幅改善
- ALNS (88.5%):比 TA-Greedy 低的原因——ALNS 优化总成本时会做 Trade-off可能为了大幅降低行驶成本而接受轻微延迟
- 这个 "Trade-off" 恰好证明了 ALNS 在多个目标之间做权衡的能力
### 3.3 CES拥堵暴露分数
**定义 (Eq.47)**CES = Σ_k Σ_(i,j)∈A_k ρ_ij(T_i)。所有车辆在所有弧上遇到的拥堵惩罚之和。
**含义**:衡量**路线是否聪明地绕开了拥堵**。高 CES = 车辆在拥堵中暴露了很多时间。
**变化趋势及原因**
- Static (3194):完全不避开拥堵 → 最高
- TA-Greedy (1789, -44%):知堵而避 → 显著降低
- ALNS (907, -49%):进一步优化 → 找到更优的绕行策略
- T-ALNS (864, -5%):记忆防循环 → 小幅优化
### 3.4 Travel Time Cost行驶时间
**含义**:纯行驶时间成本,不含迟到和拥堵惩罚。衡量路径的 "距离效率"。
**重要观察**:所有交通感知算法的行驶时间 (2307-2933) 都低于 Static (3111)。这说明 "绕开拥堵" 不等于 "绕远路"——绕开拥堵往往选择了速度更快的路径,总时间反而更短。
### 3.5 Delay Penalty迟到惩罚
**含义**:λ₁×Σ max{0, S_j-l_j}。迟到分钟数的加权和。
**变化脉络**Static (9017) → TA-Greedy (291, -97%) → ALNS (30, -90%)。交通感知消除了绝大多数迟到,元启发式搜索进一步将残余迟到降到极低水平。
### 3.6 Computation Time计算时间
**含义**:算法从开始到返回最优解的实际耗时。
**各算法对比**Static/TA-Greedy <0.1s(极快但解差)→ T-ALNS ~49s比 ALNS 的 64s 快 23%,因为不走重复路)→ T-ALNS-RRD ~52s增加了事件检测和 dispatch 开销)。
---
## 第四章:实验结果与分析
### 4.1 主对比实验
(10 seeds × 500 iterpaired t-testBonferroni 校正)
| 算法 | Total Cost | OTDR | CES | Travel | Delay | Congest |
|------|:---------:|:----:|:---:|:------:|:-----:|:-------:|
| Static-VRPTW | 15321.9 | 43.6% | 3194 | 3111 | 9017 | 3194 |
| TA-Greedy | 5013.1 | 91.3% | 1789 | 2933 | 291 | 1789 |
| ALNS-Base | 3246.8 | 88.5% | 907 | 2310 | 30 | 907 |
| T-ALNS | 3228.2 | 84.5% | 864 | 2312 | 53 | 864 |
| T-ALNS-RRD | 3336.6 | 85.1% | 982 | 2307 | 48 | 982 |
**显著性检验**
| 对比 | t 值 | p 值 | 显著性 |
|------|:----:|:----:|:------:|
| Static → TA-Greedy | 185 | <0.001 | *** |
| TA-Greedy → ALNS | 31.3 | <0.001 | *** |
| ALNS → T-ALNS | 0.47 | 0.65 | ns |
| T-ALNS → RRD | -2.41 | 0.04 | * |
**逐层分析**
**第一跳 (Static→TA, -67.3%, ***)**:延迟惩罚从 9017→291降 97%)是主要驱动力。仅 "知道何时堵车" 就能避免绝大多数迟到。CES 降低 44% 进一步验证了拥堵规避能力。
**第二跳 (TA→ALNS, -35.2%, ***)**:三项全面降低——行驶 2933→2310 (-21%)、延迟 291→30 (-90%)、拥堵 1789→907 (-49%)。元启发式全局搜索三个维度同时优化。
**第三跳 (ALNS→T-ALNS, -0.6%, ns)**:均值改善不显著,但消融实验 (4.2) 揭示了 Tabu 的深层价值。
**第四跳 (T-ALNS→RRD, +3.4%, \*)**:同步模拟下 RRD 略差。事件检测和 dispatch 的开销在当前配置下超过了收益。论文 RRD 使用独立线程并行运行,不受主循环影响。
### 4.2 Tabu 消融实验
(5 seeds × 1000 iter通过组件开关隔离贡献)
| 配置 | Cost | σ | vs ALNS | 解读 |
|------|:---:|:--:|:-------:|------|
| ALNS (无记忆) | 3213.5 | **±92.5** | baseline | 高方差,不稳定 |
| +Move Tabu | 3232.3 | **±23.9** | +0.6% | **方差降 75%** |
| +Freq Memory | 3217.0 | ±87.4 | +0.1% | 均值微降 |
| Full T-ALNS | **3207.3** | ±83.2 | **-0.2%** | 最佳均值+较稳 |
**核心发现——Move Tabu 的方差降低 75%**
这是本次复现最有说服力的结果。纯 ALNS 在不同随机种子下可能得到 3120-3310跨度 190而加了 Move Tabu 后所有解都在 3209-3256跨度 47
**机理解释**Move Tabu 通过记录近期操作并禁止重复,防止搜索在局部最优附近反复兜圈。不管从哪个随机初始解出发,最终都收敛到相近的质量水平。
**实践意义**:对需要可预测服务质量的物流系统,解的一致性与绝对质量同等重要。
**关于 "Tabu 不显著" 的正确理解**:均值不显著 (p=0.65) 是因为改善幅度小。但方差降低了 75%——Tabu 的价值在**稳定性**而非绝对成本。这恰好验证了论文的核心说法Tabu 的作用是 _"prevent cycling and enhance search diversification"_
**收敛行为**:从 1000 代收敛曲线看0-200 代所有算法快速下降200-500 代曲线开始分叉ALNS 遇到局部最优趋缓T-ALNS 继续下降500-1000 代分离更明显——**Tabu 需要充分迭代才能展现优势**。
---
## 第五章:总结
### 5.1 核心结论
1. **交通感知是最大的单一贡献者**(成本 -67%, p<0.001):在城配问题中 "知堵" 比 "优算" 更重要
2. **ALNS 进一步优化**(再降 35%, p<0.001destroy-repair 有效跳出局部最优
3. **Tabu 的核心价值在稳定性**Move Tabu 将方差降 75%,三层组合实现最佳均值
4. **RRD 受同步模拟限制**:其论文优势在于并行架构
### 5.2 关键图表
| 图 | 内容 | 文件 |
|----|------|------|
| 路线图 | 配送场景空间分布 | `fig1_route_map.png` |
| 主对比 | 四面板 (Cost/OTDR/CES/Travel) | `fig2_main_comparison.png` |
| 成本分解 | 行驶+延迟+拥堵堆叠 | `fig3_cost_breakdown.png` |
| 收敛 | ALNS vs T-ALNS vs RRD | `fig4_convergence.png` |
| 显著性 | t-test p 值热力图 | `fig5_significance.png` |
| Tabu 收敛 | 1000 代 + 末段放大 | `tabu_convergence.png` |
| Tabu 消融 | 增量贡献 + Δ 标注 | `tabu_ablation.png` |
| Tabu 稳定性 | 标准差对比 | `tabu_stability.png` |
### 5.3 局限性
- 迭代数:主对比仅 500 iterTabu 效果在 1000+ iter 才充分展现
- 路网:完全图 vs 论文 OSM 真实路网
- RRD同步模拟论文并行架构优势未体现

View File

@@ -0,0 +1,376 @@
# T-ALNS-RRD 复现优化计划
## 版本管理策略
### 目录组织
```
t_alns_rrd_reproduction/
├── configs/
│ ├── default.yaml # v1 原始配置(保留不动)
│ └── calibrated.yaml # v2 校准配置(新)
├── src/ # 核心代码(通过配置参数化,不复制)
│ └── ... # 只修改 bug不破坏 v1
├── results/
│ ├── v1_baseline/ # v1 运行结果(已生成 → 移入此处)
│ │ ├── tables/
│ │ ├── figures/
│ │ └── logs/
│ └── v2_calibrated/ # v2 运行结果(新生成)
│ ├── tables/
│ ├── figures/
│ └── logs/
└── CHANGELOG.md # 记录版本变更
```
### 运行方式:通过配置切换版本
```bash
# v1: 原始(已完成的实验)
python3 src/experiments/run_main_comparison.py --config default
# v2: 校准后
python3 src/experiments/run_main_comparison.py --config calibrated
```
输出自动路由到对应目录:
```python
output_dir = f"results/{config_name}/"
```
---
## 修改清单6 项,按实施顺序)
### 修改 1修复 Static 基线公平性 【P0 | ~10 行】
**问题**`static_vrptw.py:62` 构建路径时只用 interval 0 的旅行时间,评估时却用时间依赖时间 → Static 被不公平地"惩罚"
**修改文件**: `src/baselines/static_vrptw.py`
**改动内容**:
```python
# 原代码 (line ~60)
def _static_route_cost(self, nodes: list) -> float:
depart_time = self.ctx.op_start
for idx in range(1, len(nodes)):
i, j = nodes[idx - 1], nodes[idx]
tt = self.ctx.traffic.travel_time[i, j, 0] # ❌ 永远 interval 0
total += tt
# 新代码
def _static_route_cost(self, nodes: list) -> float:
"""使用所有时段的平均旅行时间,而非仅 interval 0"""
depart_time = self.ctx.op_start
for idx in range(1, len(nodes)):
i, j = nodes[idx - 1], nodes[idx]
# 对所有 12 个时段取平均 → 公平的"无交通感知"基线
avg_tt = np.mean([self.ctx.traffic.travel_time[i, j, h]
for h in range(self.ctx.n_intervals)
if not np.isinf(self.ctx.traffic.travel_time[i, j, h])])
tt = avg_tt
total += tt
```
**同时需要**Static 成本评估时不加 λ₁ 和 λ₂ 惩罚(只有纯旅行时间),让它和 TA-Greedy 在评估维度上可比。
**在 `cost.py` 的 `evaluate_solution` 中加一个参数**
```python
def evaluate_solution(self, solution, problem_ctx, include_penalties=True):
# Static: include_penalties=False → 只算旅行时间
# 其他: include_penalties=True → 完整 Eq.1
```
**预期效果**Static 成本从 ~7580 → ~2500-3500TA-Greedy 改善从 70% → 15-25%
---
### 修改 2CES 缩放校准 【P0 | ~5 行】
**问题**`data_generator.py:326``ρ = θ × γ`,θ=1.0,γ∈[0,1] → CES 只有 ~25。论文 CES ~1300-2850。
**修改文件**: `src/data_generator.py`
**改动内容**:
```python
# 原代码 (line ~326)
rho = self.congestion_scale * gamma # ρ ∈ [0, 1],太小
# 新代码
# ρ 应反映"拥堵带来的额外时间成本",量级应与 travel_time 可比
extra_time = base_time * (self.traffic_multipliers[h] - 1.0) # 拥堵造成的额外分钟
rho = self.congestion_scale * extra_time * gamma
# γ 作为缩放因子:[0, 1] × extra_time → ρ ∈ [0, 额外时间]
# θ 控制在 config 中设置
```
**修改文件**: `configs/calibrated.yaml`
```yaml
traffic:
congestion_scale_theta: 50.0 # v1 用 1.0 → v2 用 50.0
```
**预期效果**CES 从 ~25 → ~300-800 范围(取决于 θ 值,可通过 config 微调)
---
### 修改 3数据集难度提升 【P1 | ~15 行配置 + 生成参数】
**问题**47 客户/4 车/120kg/宽时间窗 → OTDR 100%,无优化压力
**修改文件**: `configs/calibrated.yaml`
**改动内容**:
```yaml
problem:
n_customers: 60 # 47 → 60 (+28%)
vehicle_capacity_kg: 100 # 120 → 100 (更紧)
# 其余不变
customers:
demand_min_kg: 4 # 3 → 4
demand_max_kg: 15 # 12 → 15 (更多变异性)
time_window_categories:
morning:
earliest: 540
latest: 720
afternoon:
earliest: 780
latest: 960
evening:
earliest: 1020
latest: 1200
# 每个客户窗口随机 30-90 分钟(而非 60-150
window_length_min: 30 # 新增
window_length_max: 90 # 新增
```
**修改文件**: `src/data_generator.py`(支持新配置参数)
```python
# 在 _extract_params 中添加
self.tw_window_min = c.get("window_length_min", 60)
self.tw_window_max = c.get("window_length_max", 150)
# 在 generate_customers 中使用
window_len = self.rng.uniform(self.tw_window_min, self.tw_window_max)
```
**预期效果**OTDR 从 100% → 75-90%,各算法间出现明显差异
---
### 修改 4增加迭代数与事件频率 【P1 | 配置改动】
**修改文件**: `configs/calibrated.yaml`
```yaml
alns:
max_iterations: 1000 # 500 → 1000 (对齐论文)
time_limit_sec: 600
stall_limit: 400 # 200 → 400
rrd:
rollout:
horizon_min_min: 30
horizon_max_min: 120
n_sim_min: 5 # 2 → 5
n_sim_max: 30 # 50 → 30
events:
urgency_threshold: 0.3 # 0.5 → 0.3 (更容易触发)
event_probability: 0.5 # 新增在顶层
event_check_interval: 5 # 10 → 5
```
**同时修改**: `src/rrd/event_generator.py` 让 E1 事件真正修改 travel_time 张量
```python
def generate_traffic_incident(self, current_time, solution):
arc = ...
# 临时将受影响的弧的旅行时间乘以 3
i, j = arc
saved_times = self.ctx.traffic.travel_time[i, j, :].copy()
self.ctx.traffic.travel_time[i, j, :] *= 3.0
event._undo = lambda: setattr(self.ctx.traffic, 'travel_time', ...) # rollback
return event
```
---
### 修改 5实验参数 → 30 seeds + 统计 【P2 | ~20 行】
**修改文件**: `src/experiments/run_main_comparison.py`
**改动内容**:
```python
# 1. 接受 --config 命令行参数
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--config", default="default", help="Config name")
args = parser.parse_args()
# 2. 加载对应 config
config_path = Path(f"configs/{args.config}.yaml")
with open(config_path) as f: cfg = yaml.safe_load(f)
# 3. 输出到版本目录
output_dir = Path(f"results/{args.config}/")
# 4. n_seeds 从 config 读取
n_seeds = cfg.get("experiments", {}).get("random_seeds", 30)
```
**新增**: 统计检验
```python
from scipy import stats
# 对每对算法做 paired t-test
for i in range(len(algorithms)):
for j in range(i+1, len(algorithms)):
t_stat, p_val = stats.ttest_rel(costs_i, costs_j)
print(f" {names[i]} vs {names[j]}: t={t_stat:.2f}, p={p_val:.4f}")
```
**修改文件**: `requirements.txt`
```
scipy>=1.10 # 新增
```
---
### 修改 6实验结果版本隔离 【架构 | ~10 行】
**修改文件**: `src/experiments/run_main_comparison.py`
**改动内容**:
```python
def run_experiment(config_name="default", ...):
output_dir = Path(f"results/{config_name}/")
output_dir.mkdir(parents=True, exist_ok=True)
(output_dir / "tables").mkdir(exist_ok=True)
(output_dir / "figures").mkdir(exist_ok=True)
(output_dir / "logs").mkdir(exist_ok=True)
# 保存一份 config 副本到结果目录
import shutil
shutil.copy(f"configs/{config_name}.yaml", output_dir / "config_used.yaml")
# ... 后续所有输出路径都基于 output_dir
```
---
## 完整文件变更清单
| # | 文件 | 操作 | 内容 |
|---|------|------|------|
| 1 | `configs/calibrated.yaml` | **新建** | v2 完整配置(复制 default + 修改 6 个参数组) |
| 2 | `src/baselines/static_vrptw.py` | 修改 10 行 | 使用平均旅行时间 + 移除评估中的 λ 惩罚 |
| 3 | `src/cost.py` | 修改 5 行 | evaluate_solution 加 `include_penalties` 参数 |
| 4 | `src/data_generator.py` | 修改 8 行 | 客户生成支持 window_length_min/maxρ 改用 extra_time |
| 5 | `src/rrd/event_generator.py` | 修改 15 行 | E1 事件真正修改 travel time 张量 |
| 6 | `src/experiments/run_main_comparison.py` | 修改 30 行 | 支持 --config版本输出目录统计检验 |
| 7 | `src/visualization/plot_results.py` | 修改 5 行 | 接受 output_dir 参数 |
| 8 | `requirements.txt` | 修改 1 行 | 添加 scipy |
| 9 | `CHANGELOG.md` | **新建** | 记录 v1/v2 差异 |
| 10 | `results/v1_baseline/` | 移动 | 将已有 v1 结果移入子目录 |
**不动的文件**v1 和 v2 共用):
```
src/problem.py (无 bug不碰)
src/alns/* (无 bug参数通过 config 控制)
src/tabu/* (无 bug参数通过 config 控制)
src/rrd/dispatch.py (无 bug)
src/rrd/rollout.py (无 bug)
src/rrd/candidate_actions.py (无 bug)
```
---
## 实施步骤6 步,约 1-2 小时)
### Step 0: 版本快照 + 目录准备
```bash
cd t_alns_rrd_reproduction
# 0.1 创建 CHANGELOG
echo "# Changelog\n\n## v1.0-baseline (default)\n- 47 customers, 4 vehicles, 120kg\n- θ=1.0, 200 iter, 5 seeds, complete graph\n- Known issues: inflated Static baseline, low CES, OTDR=100%\n" > CHANGELOG.md
# 0.2 隔离 v1 结果
mkdir -p results/v1_baseline
mv results/tables results/figures results/logs results/v1_baseline/ 2>/dev/null
mkdir -p results/v1_baseline/{tables,figures,logs}
```
### Step 1: 创建 calibrated.yaml
复制 `configs/default.yaml` → 修改以上所有参数
### Step 2: 修改 static_vrptw.py + cost.py
修复 Static 基线公平性(两个文件,共约 15 行改动)
### Step 3: 修改 data_generator.py
支持新客户窗口参数 + 修复 ρ 计算(一个文件,约 10 行改动)
### Step 4: 修改 event_generator.py
让 E1 事件真正影响 travel time一个文件约 15 行改动)
### Step 5: 修改 run_main_comparison.py + 依赖
支持 --config、版本目录、统计检验、scipy一个文件 + requirements.txt
### Step 6: 生成 v2 数据 + 跑实验
```bash
# 6.1 重新生成校准数据集
python3 -c "from src.data_generator import DataGenerator; DataGenerator(config_path='configs/calibrated.yaml', seed=42).generate_all()"
# 6.2 跑 v2 实验30 seeds × 1000 iter ~ 20-30 分钟)
python3 src/experiments/run_main_comparison.py --config calibrated
```
---
## 预期结果对比v1 vs v2
| 指标 | v1 当前 | v2 预期 | 变化原因 |
|------|---------|---------|---------|
| Static Cost | 7580 | 2500-3500 | 用平均时间,移除评估偏差 |
| TA-Greedy Cost | 2250 | 2000-2500 | 更难的数据集 |
| OTDR (all algos) | 100% | 75-95% | 更紧时间窗 + 更多客户 |
| CES (all algos) | 22-30 | 300-800 | θ=50 + ρ 改用 extra_time |
| Static→TA drop | 70% | 15-25% | 基线公平后差距缩小 |
| ALNS→T-ALNS drop | 0.2% | 2-5% | 更多迭代让 Tabu 生效 |
| T-ALNS→RRD drop | 0.1% | 1-3% | 更多事件 + 事件真正改 travel time |
| n_seeds | 5 | 30 | 对齐论文 |
| p-values | 无 | < 0.05 | 添加 t-test |
---
## 风险控制
| 风险 | 缓解措施 |
|------|---------|
| v2 改动破坏 v1 | 所有改动通过 config 参数化,不改 v1 逻辑路径 |
| 数据集变难导致无可行解 | 先保守收紧n=55, Q=110不行再松 |
| 跑 30 seeds 太慢 | 先用 10 seeds 快速验证,确认趋势正确后再跑全量 |
| CES 调到什么值合适 | 先设 θ=30 跑一轮看效果,再调 |
---
## 对比报告模板
完成后 `results/` 目录结构:
```
results/
├── v1_baseline/
│ ├── tables/main_comparison.csv
│ ├── figures/fig1-7.png
│ ├── logs/convergence.npz
│ └── config_used.yaml
├── v2_calibrated/
│ ├── tables/main_comparison.csv
│ ├── figures/fig1-7.png
│ ├── logs/convergence.npz
│ └── config_used.yaml
└── comparison_report.md # 自动生成的 v1 vs v2 对比
```
对比报告包含:
- 两张表并排v1 数值 vs v2 数值)
- 趋势一致性检查(每个算法的递进方向)
- 统计显著性v2 的 t-test 结果)
- 结论v2 更接近论文报告的质量标准

View File

@@ -0,0 +1,116 @@
# T-ALNS-RRD Reproduction
Reproduction of the paper **"Optimizing urban last mile delivery efficiency through dynamic vehicle routing heuristics and traffic flow analysis"** (Liu & Wang, 2025).
## Overview
This project reproduces the core algorithmic framework of T-ALNS-RRD using a **custom synthetic dataset** (methodological reproduction). The reproduction validates the following trends:
1. Traffic-aware cost reduces congestion exposure
2. ALNS provides further cost reduction over greedy heuristics
3. Tabu memory improves solution stability
4. RRD real-time dispatch handles disruption events
## Project Structure
```
t_alns_rrd_reproduction/
├── configs/default.yaml # All parameters (aligned with paper)
├── src/
│ ├── data_generator.py # Synthetic data generation
│ ├── problem.py # DVRPTW-TA formulation
│ ├── cost.py # Cost functions (Eq.1, Eq.16-17)
│ ├── baselines/
│ │ ├── static_vrptw.py # Static VRPTW greedy
│ │ └── ta_greedy.py # Traffic-aware greedy
│ ├── alns/
│ │ ├── operators_destroy.py # Destroy operators (random/worst/related)
│ │ ├── operators_repair.py # Repair operators (greedy/regret2/time-window)
│ │ ├── acceptance.py # Simulated annealing
│ │ └── alns_base.py # ALNS-Base (Algorithm 1)
│ ├── tabu/
│ │ ├── move_tabu.py # Move-based Tabu (Eq.22-23)
│ │ ├── solution_tabu.py # Solution hash memory (Eq.24)
│ │ ├── frequency_memory.py # Frequency matrices (Eq.25-26)
│ │ └── t_alns.py # T-ALNS (Algorithm 2)
│ ├── rrd/
│ │ ├── event_generator.py # Event detection (Eq.35)
│ │ ├── candidate_actions.py # Action generation
│ │ ├── rollout.py # Rollout simulation (Eq.36-39)
│ │ ├── dispatch.py # Dispatch decision (Eq.40-42)
│ │ └── t_alns_rrd.py # T-ALNS-RRD (Algorithm 3)
│ ├── experiments/
│ │ └── run_main_comparison.py
│ └── visualization/
│ └── plot_results.py
├── data/synthetic/ # Generated datasets
├── results/ # Experiment outputs
└── requirements.txt
```
## Quick Start
```bash
# Install dependencies
pip install -r requirements.txt
# Generate data and run quick test
python3 -c "
from src.data_generator import DataGenerator
from src.problem import ProblemContext
from src.cost import CostCalculator
from src.alns.alns_base import ALNSBase
from src.tabu.t_alns import TALNS
data = DataGenerator(seed=42).generate_all()
ctx = ProblemContext(
customers={c.customer_id: c for c in data['customers']},
depot=data['depot'], traffic=data['traffic'],
n_vehicles=data['n_vehicles'], vehicle_capacity=data['vehicle_capacity'],
)
cc = CostCalculator()
alns = ALNSBase(ctx, cc, config={'max_iterations': 100, 'time_limit_sec': 60})
result = alns.run(seed=42)
print(f'ALNS: Cost={result[\"total_cost\"]:.1f} OTDR={result[\"otdr\"]*100:.1f}%')
talns = TALNS(ctx, cc, config={'max_iterations': 100, 'time_limit_sec': 60})
result2 = talns.run(seed=42)
print(f'T-ALNS: Cost={result2[\"total_cost\"]:.1f} OTDR={result2[\"otdr\"]*100:.1f}%')
"
# Run full comparison experiment
python3 src/experiments/run_main_comparison.py
```
## Key Algorithms
| Algorithm | Description | Paper Reference |
|-----------|-------------|-----------------|
| Static-VRPTW | Greedy with static travel times | Baseline 1 |
| TA-VRPTW-Greedy | Greedy with time-dependent costs | Baseline 2 |
| ALNS-Base | Adaptive destroy-repair with SA | Algorithm 1 |
| T-ALNS | ALNS + 3-layer Tabu memory | Algorithm 2 |
| T-ALNS-RRD | T-ALNS + rollout-based dispatch | Algorithm 3 |
## Experiment Configurations
- Dataset: 47 customers, 1 depot, 4 vehicles (120kg capacity)
- Traffic: 12 one-hour intervals, 6:00-18:00
- Area: 8 × 10 km² urban zone
- Complete graph: 2,256 arcs, 81,200+ traffic data points
## Expected Trends
Results should show progressive improvement:
```
Static-VRPTW (highest cost, lowest OTDR)
→ TA-Greedy (reduced congestion)
→ ALNS-Base (further cost reduction)
→ T-ALNS (better stability)
→ T-ALNS-RRD (best under disruptions)
```
## Reproduction Statement
> Since the original dataset requires reasonable request to the authors and is not yet publicly available, this project constructs a custom synthetic dataset matching the experimental scale and structure described in the paper to reproduce its core algorithmic framework and comparative experimental logic. This is a **methodological reproduction** focused on verifying the relative impact of different algorithmic modules, not an exact numerical replication.

View File

@@ -0,0 +1,149 @@
# Calibrated configuration (v2) for T-ALNS-RRD Reproduction
# Key changes from default:
# - 60 customers (was 47), 100kg capacity (was 120kg)
# - Tighter time windows (30-90min vs 60-150min)
# - θ=50 for CES scaling (was 1.0)
# - 1000 iterations, 30 seeds, statistical testing
# - More frequent & impactful RRD events
# Problem scale - HARDER THAN DEFAULT
problem:
n_customers: 55
n_vehicles: 4
depot_count: 1
vehicle_capacity_kg: 115 # 120 → 115 (slightly tighter, still feasible)
service_time_min: 4
area_width_km: 8.0
area_height_km: 10.0
operating_start: 360
operating_end: 1080
n_time_intervals: 12
# Customer generation - MORE CONSTRAINED
customers:
demand_min_kg: 3
demand_max_kg: 13 # 3 → 13 (was 15 - keep feasible)
window_length_min: 30 # NEW: shortest window (min)
window_length_max: 90 # NEW: longest window (min)
time_window_categories:
morning:
earliest: 540
latest: 720
afternoon:
earliest: 780
latest: 960
evening:
earliest: 1020
latest: 1200
num_clusters: 3
cluster_labels: ["residential", "commercial", "office"]
# Road network (unchanged)
roads:
types:
arterial:
speed_kmh: 45
proportion: 0.25
collector:
speed_kmh: 30
proportion: 0.35
residential:
speed_kmh: 20
proportion: 0.40
noise_std: 0.05
use_complete_graph: true
# Traffic congestion - SCALED CES
traffic:
multipliers: [1.0, 1.0, 1.6, 1.6, 1.2, 1.2, 1.0, 1.0, 1.2, 1.2, 1.7, 1.7]
congestion_scale_theta: 50.0 # 1.0 → 50.0 (CES into paper range)
risk_aversion_beta: 0.3
uncertainty_base: 0.05
# Cost function weights (unchanged)
cost:
lambda_lateness: 1.0
lambda_congestion: 1.0
lambda_stability: 0.3
# ALNS parameters - MORE ITERATIONS
alns:
max_iterations: 1000 # Always run full iterations
time_limit_sec: 600
destroy_ratio_min: 0.1
destroy_ratio_max: 0.4
initial_temperature_factor: 0.05
cooling_rate: 0.99975
reaction_factor: 0.1
segment_length: 100
stall_limit: 400 # 200 → 400 (allow longer search)
max_attempts: 5
reward_global_best: 1.0
reward_improvement: 0.5
reward_accepted: 0.2
reward_rejected: 0.0
# Tabu memory parameters (unchanged)
tabu:
move_tabu:
tenure: 7
tenure_min: 3
tenure_max: 12
overlap_threshold: 0.5
stall_for_increase: 50
solution_tabu:
tenure: 15
buffer_size: 1000
hash_prime: 1000000007
frequency:
normalization_factor: 2
normalization_interval: 50
diversification:
delta_max: 0.7
eta_balance: 0.5
weights: [0.4, 0.3, 0.3]
aspiration:
beta_threshold: 0.3
gamma_threshold: 0.8
# RRD parameters - MORE EVENTS
rrd:
rollout:
horizon_min_min: 30
horizon_max_min: 120
urgency_alpha: 1.0
n_sim_min: 5 # 2 → 5
n_sim_max: 30 # 50 → 30
mc_iterations: 50
time_overhead_ms: 10
time_per_sim_ms: 50
dispatch:
weight_rollout: 0.4
weight_stability: 0.3
weight_recovery: 0.3
tabu:
penalty: 50.0
bonus: 25.0
events:
urgency_threshold: 0.3 # 0.5 → 0.3 (easier to trigger)
event_probability: 0.5 # NEW: event check probability
event_check_interval: 5 # NEW: check every N iterations
weights:
E1_traffic: [0.5, 0.3, 0.2]
E2_urgent: [0.7, 0.1, 0.2]
E3_capacity: [0.3, 0.4, 0.3]
E4_timewindow: [0.8, 0.1, 0.1]
max_actions: 20
# Experiment settings - MORE SEEDS + STATISTICAL TESTING
experiments:
random_seeds: 30 # 5 → 30 (paper standard)
seed_start: 1
report_mean_std: true
statistical_testing: true # NEW: run paired t-tests
sensitivity:
fleet_sizes: [2, 3, 4, 5, 6]
customer_counts: [30, 40, 47, 60]
capacities: [80, 100, 120, 140, 160]
robustness:
sigma_values: [0.1, 0.2, 0.3, 0.5]

View File

@@ -0,0 +1,145 @@
# Default configuration for T-ALNS-RRD Reproduction
# All parameters aligned with the original paper.
# Problem scale (paper §4.2)
problem:
n_customers: 47
n_vehicles: 4
depot_count: 1
vehicle_capacity_kg: 120
service_time_min: 4
area_width_km: 8.0
area_height_km: 10.0
operating_start: 360 # 6:00 AM in minutes (0 = midnight)
operating_end: 1080 # 6:00 PM in minutes
n_time_intervals: 12 # H = 12 one-hour intervals
# Customer generation (paper §4.2)
customers:
demand_min_kg: 3
demand_max_kg: 12
time_window_categories:
morning:
earliest: 540 # 9:00
latest: 720 # 12:00
afternoon:
earliest: 780 # 13:00
latest: 960 # 16:00
evening:
earliest: 1020 # 17:00
latest: 1200 # 20:00
num_clusters: 3
cluster_labels: ["residential", "commercial", "office"]
# Road network (paper §3.2)
roads:
types:
arterial:
speed_kmh: 45
proportion: 0.25
collector:
speed_kmh: 30
proportion: 0.35
residential:
speed_kmh: 20
proportion: 0.40
noise_std: 0.05 # base travel time noise
use_complete_graph: true
# Traffic congestion (paper §3.2, Table in §4.3)
traffic:
# hourly congestion multipliers (6:00-7:00, ..., 17:00-18:00)
multipliers: [1.0, 1.0, 1.6, 1.6, 1.2, 1.2, 1.0, 1.0, 1.2, 1.2, 1.7, 1.7]
congestion_scale_theta: 1.0 # θ in Eq.9
risk_aversion_beta: 0.3 # β in Eq.10
uncertainty_base: 0.05 # base uncertainty proportion of travel time
# Cost function weights (paper Eq.1)
cost:
lambda_lateness: 1.0 # λ₁
lambda_congestion: 1.0 # λ₂
lambda_stability: 0.3 # λ₃ (for RRD only)
# ALNS parameters (paper §3.3.1)
alns:
max_iterations: 1000
time_limit_sec: 600
destroy_ratio_min: 0.1 # α_min
destroy_ratio_max: 0.4 # α_max
initial_temperature_factor: 0.05 # T₀ = factor × Z(S⁰)
cooling_rate: 0.99975 # γ in Eq.21
reaction_factor: 0.1 # ξ in Eq.19
segment_length: 100 # π iterations for weight update
stall_limit: 200 # T_stall consecutive no-improvement
max_attempts: 5 # max attempts per candidate generation
# Reward levels (σ₁, σ₂, σ₃, σ₄)
reward_global_best: 1.0
reward_improvement: 0.5
reward_accepted: 0.2
reward_rejected: 0.0
# Tabu memory parameters (paper §3.3.2)
tabu:
move_tabu:
tenure: 7 # τ_move initial
tenure_min: 3
tenure_max: 12
overlap_threshold: 0.5 # μ in Eq.23
stall_for_increase: 50 # τ_stall for adaptive tenure
solution_tabu:
tenure: 15 # τ_sol
buffer_size: 1000
hash_prime: 1000000007
frequency:
normalization_factor: 2 # κ in Eq.33
normalization_interval: 50 # ν in Eq.33
diversification:
delta_max: 0.7 # δ_max in Eq.27
eta_balance: 0.5 # η in Eq.28
weights: [0.4, 0.3, 0.3] # ω₁, ω₂, ω₃ in Eq.27
aspiration:
beta_threshold: 0.3 # β in Eq.30
gamma_threshold: 0.8 # γ in Eq.31
# RRD parameters (paper §3.3.3)
rrd:
rollout:
horizon_min_min: 30 # H_min
horizon_max_min: 120 # H_max
urgency_alpha: 1.0 # α_urgency
n_sim_min: 2 # N_min
n_sim_max: 50 # N_max
mc_iterations: 50 # Monte Carlo iterations
time_overhead_ms: 10
time_per_sim_ms: 50
dispatch:
# Weights for composite score (Eq.40)
weight_rollout: 0.4 # ω₁
weight_stability: 0.3 # ω₂
weight_recovery: 0.3 # ω₃
tabu:
penalty: 50.0 # τ_penalty in Eq.39
bonus: 25.0 # τ_bonus in Eq.39
events:
urgency_threshold: 0.5 # trigger threshold
# Event weights (α_e, β_e, γ_e in Eq.35)
weights:
E1_traffic: [0.5, 0.3, 0.2]
E2_urgent: [0.7, 0.1, 0.2]
E3_capacity: [0.3, 0.4, 0.3]
E4_timewindow: [0.8, 0.1, 0.1]
max_actions: 20 # |A_e| ≤ 20
# Experiment settings (paper §4.8, §5)
experiments:
random_seeds: 30 # 30 runs per configuration
seed_start: 1 # seed range: 1..30
report_mean_std: true
# Sensitivity analysis ranges (paper §5.1)
sensitivity:
fleet_sizes: [2, 3, 4, 5, 6]
customer_counts: [30, 40, 47, 60]
capacities: [80, 100, 120, 140, 160]
# Uncertainty levels (paper §5.2)
robustness:
sigma_values: [0.1, 0.2, 0.3, 0.5]

View File

@@ -0,0 +1,7 @@
numpy>=1.24
pandas>=2.0
matplotlib>=3.7
networkx>=3.1
pyyaml>=6.0
tqdm>=4.65
scipy>=1.10

View File

@@ -0,0 +1,149 @@
# Calibrated configuration (v2) for T-ALNS-RRD Reproduction
# Key changes from default:
# - 60 customers (was 47), 100kg capacity (was 120kg)
# - Tighter time windows (30-90min vs 60-150min)
# - θ=50 for CES scaling (was 1.0)
# - 1000 iterations, 30 seeds, statistical testing
# - More frequent & impactful RRD events
# Problem scale - HARDER THAN DEFAULT
problem:
n_customers: 55
n_vehicles: 4
depot_count: 1
vehicle_capacity_kg: 115 # 120 → 115 (slightly tighter, still feasible)
service_time_min: 4
area_width_km: 8.0
area_height_km: 10.0
operating_start: 360
operating_end: 1080
n_time_intervals: 12
# Customer generation - MORE CONSTRAINED
customers:
demand_min_kg: 3
demand_max_kg: 13 # 3 → 13 (was 15 - keep feasible)
window_length_min: 30 # NEW: shortest window (min)
window_length_max: 90 # NEW: longest window (min)
time_window_categories:
morning:
earliest: 540
latest: 720
afternoon:
earliest: 780
latest: 960
evening:
earliest: 1020
latest: 1200
num_clusters: 3
cluster_labels: ["residential", "commercial", "office"]
# Road network (unchanged)
roads:
types:
arterial:
speed_kmh: 45
proportion: 0.25
collector:
speed_kmh: 30
proportion: 0.35
residential:
speed_kmh: 20
proportion: 0.40
noise_std: 0.05
use_complete_graph: true
# Traffic congestion - SCALED CES
traffic:
multipliers: [1.0, 1.0, 1.6, 1.6, 1.2, 1.2, 1.0, 1.0, 1.2, 1.2, 1.7, 1.7]
congestion_scale_theta: 50.0 # 1.0 → 50.0 (CES into paper range)
risk_aversion_beta: 0.3
uncertainty_base: 0.05
# Cost function weights (unchanged)
cost:
lambda_lateness: 1.0
lambda_congestion: 1.0
lambda_stability: 0.3
# ALNS parameters - MORE ITERATIONS
alns:
max_iterations: 1000 # Always run full iterations
time_limit_sec: 600
destroy_ratio_min: 0.1
destroy_ratio_max: 0.4
initial_temperature_factor: 0.05
cooling_rate: 0.99975
reaction_factor: 0.1
segment_length: 100
stall_limit: 400 # 200 → 400 (allow longer search)
max_attempts: 5
reward_global_best: 1.0
reward_improvement: 0.5
reward_accepted: 0.2
reward_rejected: 0.0
# Tabu memory parameters (unchanged)
tabu:
move_tabu:
tenure: 7
tenure_min: 3
tenure_max: 12
overlap_threshold: 0.5
stall_for_increase: 50
solution_tabu:
tenure: 15
buffer_size: 1000
hash_prime: 1000000007
frequency:
normalization_factor: 2
normalization_interval: 50
diversification:
delta_max: 0.7
eta_balance: 0.5
weights: [0.4, 0.3, 0.3]
aspiration:
beta_threshold: 0.3
gamma_threshold: 0.8
# RRD parameters - MORE EVENTS
rrd:
rollout:
horizon_min_min: 30
horizon_max_min: 120
urgency_alpha: 1.0
n_sim_min: 5 # 2 → 5
n_sim_max: 30 # 50 → 30
mc_iterations: 50
time_overhead_ms: 10
time_per_sim_ms: 50
dispatch:
weight_rollout: 0.4
weight_stability: 0.3
weight_recovery: 0.3
tabu:
penalty: 50.0
bonus: 25.0
events:
urgency_threshold: 0.3 # 0.5 → 0.3 (easier to trigger)
event_probability: 0.5 # NEW: event check probability
event_check_interval: 5 # NEW: check every N iterations
weights:
E1_traffic: [0.5, 0.3, 0.2]
E2_urgent: [0.7, 0.1, 0.2]
E3_capacity: [0.3, 0.4, 0.3]
E4_timewindow: [0.8, 0.1, 0.1]
max_actions: 20
# Experiment settings - MORE SEEDS + STATISTICAL TESTING
experiments:
random_seeds: 30 # 5 → 30 (paper standard)
seed_start: 1
report_mean_std: true
statistical_testing: true # NEW: run paired t-tests
sensitivity:
fleet_sizes: [2, 3, 4, 5, 6]
customer_counts: [30, 40, 47, 60]
capacities: [80, 100, 120, 140, 160]
robustness:
sigma_values: [0.1, 0.2, 0.3, 0.5]

View File

@@ -0,0 +1,6 @@
algorithm,total_cost_mean,total_cost_std,otdr_mean,otdr_std,ces_mean,ces_std,travel_time_mean,delay_penalty_mean,congestion_cost_mean,computation_time_mean,avg_delay_mean,max_delay_mean,late_customers_mean
Static-VRPTW,15321.894699999999,1.9173831849720224e-12,43.63636363636363,0.0,3193.5278,0.0,3111.4945,9016.872399999997,3193.5278,0.1911109209060669,300.5624133333332,512.7649999999998,30.0
TA-VRPTW-Greedy,5013.09167,175.89575187422835,91.27272727272727,3.6161051854972857,1789.1400299999998,75.81980551459641,2932.68725,291.26438999999993,1789.1400299999998,0.027298784255981444,67.99000710714286,167.66838,4.6
ALNS-Base,3246.7716299999997,104.20460175913583,88.54545454545455,2.7171529419951392,906.95191,49.998293085829985,2309.61222,30.207500000000017,906.95191,63.5413857460022,7.706254285714287,14.838140000000044,4.3
T-ALNS,3228.2472500000003,85.46898350558733,84.54545454545453,3.1198879215112094,863.8959600000001,45.765308168456315,2311.5525900000002,52.79870000000001,863.8959600000001,48.58432672023773,8.081892531746034,19.420940000000087,6.5
T-ALNS-RRD,3336.590004828589,160.19969851781602,85.09090909090908,2.064168044354714,981.6906182031265,117.47264426552245,2306.6826584178852,48.21672820757747,981.6906182031265,51.64886746406555,7.734138309051906,21.168398007750977,6.1
1 algorithm total_cost_mean total_cost_std otdr_mean otdr_std ces_mean ces_std travel_time_mean delay_penalty_mean congestion_cost_mean computation_time_mean avg_delay_mean max_delay_mean late_customers_mean
2 Static-VRPTW 15321.894699999999 1.9173831849720224e-12 43.63636363636363 0.0 3193.5278 0.0 3111.4945 9016.872399999997 3193.5278 0.1911109209060669 300.5624133333332 512.7649999999998 30.0
3 TA-VRPTW-Greedy 5013.09167 175.89575187422835 91.27272727272727 3.6161051854972857 1789.1400299999998 75.81980551459641 2932.68725 291.26438999999993 1789.1400299999998 0.027298784255981444 67.99000710714286 167.66838 4.6
4 ALNS-Base 3246.7716299999997 104.20460175913583 88.54545454545455 2.7171529419951392 906.95191 49.998293085829985 2309.61222 30.207500000000017 906.95191 63.5413857460022 7.706254285714287 14.838140000000044 4.3
5 T-ALNS 3228.2472500000003 85.46898350558733 84.54545454545453 3.1198879215112094 863.8959600000001 45.765308168456315 2311.5525900000002 52.79870000000001 863.8959600000001 48.58432672023773 8.081892531746034 19.420940000000087 6.5
6 T-ALNS-RRD 3336.590004828589 160.19969851781602 85.09090909090908 2.064168044354714 981.6906182031265 117.47264426552245 2306.6826584178852 48.21672820757747 981.6906182031265 51.64886746406555 7.734138309051906 21.168398007750977 6.1

View File

@@ -0,0 +1,11 @@
algo_a,algo_b,t_statistic,p_value,significant
Static-VRPTW,TA-VRPTW-Greedy,185.3330576633527,1.9716478558253748e-17,***
Static-VRPTW,ALNS-Base,366.44151297950805,4.2727722775178273e-20,***
Static-VRPTW,T-ALNS,447.4543816071859,7.080306899468622e-21,***
Static-VRPTW,T-ALNS-RRD,236.58509746594012,2.191284240436559e-18,***
TA-VRPTW-Greedy,ALNS-Base,31.25967939768836,1.720760601298381e-10,***
TA-VRPTW-Greedy,T-ALNS,29.4464353247755,2.93244000878877e-10,***
TA-VRPTW-Greedy,T-ALNS-RRD,24.6826583540389,1.4102196456675431e-09,***
ALNS-Base,T-ALNS,0.46670227005865406,0.6518039930669947,ns
ALNS-Base,T-ALNS-RRD,-1.515866438976666,0.1638582485938116,ns
T-ALNS,T-ALNS-RRD,-2.4093905940604805,0.03928826243785392,*
1 algo_a algo_b t_statistic p_value significant
2 Static-VRPTW TA-VRPTW-Greedy 185.3330576633527 1.9716478558253748e-17 ***
3 Static-VRPTW ALNS-Base 366.44151297950805 4.2727722775178273e-20 ***
4 Static-VRPTW T-ALNS 447.4543816071859 7.080306899468622e-21 ***
5 Static-VRPTW T-ALNS-RRD 236.58509746594012 2.191284240436559e-18 ***
6 TA-VRPTW-Greedy ALNS-Base 31.25967939768836 1.720760601298381e-10 ***
7 TA-VRPTW-Greedy T-ALNS 29.4464353247755 2.93244000878877e-10 ***
8 TA-VRPTW-Greedy T-ALNS-RRD 24.6826583540389 1.4102196456675431e-09 ***
9 ALNS-Base T-ALNS 0.46670227005865406 0.6518039930669947 ns
10 ALNS-Base T-ALNS-RRD -1.515866438976666 0.1638582485938116 ns
11 T-ALNS T-ALNS-RRD -2.4093905940604805 0.03928826243785392 *

View File

@@ -0,0 +1,149 @@
# Calibrated configuration (v2) for T-ALNS-RRD Reproduction
# Key changes from default:
# - 60 customers (was 47), 100kg capacity (was 120kg)
# - Tighter time windows (30-90min vs 60-150min)
# - θ=50 for CES scaling (was 1.0)
# - 1000 iterations, 30 seeds, statistical testing
# - More frequent & impactful RRD events
# Problem scale - HARDER THAN DEFAULT
problem:
n_customers: 55
n_vehicles: 4
depot_count: 1
vehicle_capacity_kg: 115 # 120 → 115 (slightly tighter, still feasible)
service_time_min: 4
area_width_km: 8.0
area_height_km: 10.0
operating_start: 360
operating_end: 1080
n_time_intervals: 12
# Customer generation - MORE CONSTRAINED
customers:
demand_min_kg: 3
demand_max_kg: 13 # 3 → 13 (was 15 - keep feasible)
window_length_min: 30 # NEW: shortest window (min)
window_length_max: 90 # NEW: longest window (min)
time_window_categories:
morning:
earliest: 540
latest: 720
afternoon:
earliest: 780
latest: 960
evening:
earliest: 1020
latest: 1200
num_clusters: 3
cluster_labels: ["residential", "commercial", "office"]
# Road network (unchanged)
roads:
types:
arterial:
speed_kmh: 45
proportion: 0.25
collector:
speed_kmh: 30
proportion: 0.35
residential:
speed_kmh: 20
proportion: 0.40
noise_std: 0.05
use_complete_graph: true
# Traffic congestion - SCALED CES
traffic:
multipliers: [1.0, 1.0, 1.6, 1.6, 1.2, 1.2, 1.0, 1.0, 1.2, 1.2, 1.7, 1.7]
congestion_scale_theta: 50.0 # 1.0 → 50.0 (CES into paper range)
risk_aversion_beta: 0.3
uncertainty_base: 0.05
# Cost function weights (unchanged)
cost:
lambda_lateness: 1.0
lambda_congestion: 1.0
lambda_stability: 0.3
# ALNS parameters - MORE ITERATIONS
alns:
max_iterations: 1000 # Always run full iterations
time_limit_sec: 600
destroy_ratio_min: 0.1
destroy_ratio_max: 0.4
initial_temperature_factor: 0.05
cooling_rate: 0.99975
reaction_factor: 0.1
segment_length: 100
stall_limit: 400 # 200 → 400 (allow longer search)
max_attempts: 5
reward_global_best: 1.0
reward_improvement: 0.5
reward_accepted: 0.2
reward_rejected: 0.0
# Tabu memory parameters (unchanged)
tabu:
move_tabu:
tenure: 7
tenure_min: 3
tenure_max: 12
overlap_threshold: 0.5
stall_for_increase: 50
solution_tabu:
tenure: 15
buffer_size: 1000
hash_prime: 1000000007
frequency:
normalization_factor: 2
normalization_interval: 50
diversification:
delta_max: 0.7
eta_balance: 0.5
weights: [0.4, 0.3, 0.3]
aspiration:
beta_threshold: 0.3
gamma_threshold: 0.8
# RRD parameters - MORE EVENTS
rrd:
rollout:
horizon_min_min: 30
horizon_max_min: 120
urgency_alpha: 1.0
n_sim_min: 5 # 2 → 5
n_sim_max: 30 # 50 → 30
mc_iterations: 50
time_overhead_ms: 10
time_per_sim_ms: 50
dispatch:
weight_rollout: 0.4
weight_stability: 0.3
weight_recovery: 0.3
tabu:
penalty: 50.0
bonus: 25.0
events:
urgency_threshold: 0.3 # 0.5 → 0.3 (easier to trigger)
event_probability: 0.5 # NEW: event check probability
event_check_interval: 5 # NEW: check every N iterations
weights:
E1_traffic: [0.5, 0.3, 0.2]
E2_urgent: [0.7, 0.1, 0.2]
E3_capacity: [0.3, 0.4, 0.3]
E4_timewindow: [0.8, 0.1, 0.1]
max_actions: 20
# Experiment settings - MORE SEEDS + STATISTICAL TESTING
experiments:
random_seeds: 30 # 5 → 30 (paper standard)
seed_start: 1
report_mean_std: true
statistical_testing: true # NEW: run paired t-tests
sensitivity:
fleet_sizes: [2, 3, 4, 5, 6]
customer_counts: [30, 40, 47, 60]
capacities: [80, 100, 120, 140, 160]
robustness:
sigma_values: [0.1, 0.2, 0.3, 0.5]

View File

@@ -0,0 +1,5 @@
configuration,total_cost_mean,total_cost_std,otdr_mean,otdr_std,ces_mean,ces_std,computation_time_mean,iterations_mean
ALNS-Base (no Tabu),3213.5472600000003,92.47828809019447,86.54545454545455,3.302891295379083,873.8731200000002,72.66381047359131,83.00211868286132,656.4
+ Move Tabu only,3232.33156,23.85173277307108,86.18181818181817,3.042400096487549,884.6135199999999,23.691658934148084,51.74106793403625,472.8
+ Frequency Memory only,3216.9874600000003,87.42641474241665,87.63636363636364,3.726163914894401,892.23014,36.74464052488473,67.69878606796264,532.6
Full T-ALNS,3207.3305,83.1845531049185,83.63636363636363,2.8747978728803405,859.0403399999999,44.94860445801399,56.30859026908875,499.4
1 configuration total_cost_mean total_cost_std otdr_mean otdr_std ces_mean ces_std computation_time_mean iterations_mean
2 ALNS-Base (no Tabu) 3213.5472600000003 92.47828809019447 86.54545454545455 3.302891295379083 873.8731200000002 72.66381047359131 83.00211868286132 656.4
3 + Move Tabu only 3232.33156 23.85173277307108 86.18181818181817 3.042400096487549 884.6135199999999 23.691658934148084 51.74106793403625 472.8
4 + Frequency Memory only 3216.9874600000003 87.42641474241665 87.63636363636364 3.726163914894401 892.23014 36.74464052488473 67.69878606796264 532.6
5 Full T-ALNS 3207.3305 83.1845531049185 83.63636363636363 2.8747978728803405 859.0403399999999 44.94860445801399 56.30859026908875 499.4

View File

@@ -0,0 +1,6 @@
algorithm,total_cost_mean,total_cost_std,otdr_mean,otdr_std,ces_mean,ces_std,travel_time_mean,delay_penalty_mean,congestion_cost_mean,computation_time_mean,computation_time_std,avg_delay_mean,max_delay_mean,late_customers_mean
Static-VRPTW,7580.3396999999995,1.016845989170083e-12,68.08510638297872,0.0,30.313699999999994,3.972054645195637e-15,3044.2300999999998,4505.795900000001,30.313699999999994,0.004783535003662109,0.00018088214390829616,300.3863933333334,493.0151000000001,15.0
TA-VRPTW-Greedy,2217.5418,7.227591305891663,100.0,0.0,22.31862,0.32531771700907924,2195.22318,0.0,22.31862,0.028783178329467772,0.002923092418020413,0.0,0.0,0.0
ALNS-Base,1897.9593999999997,64.64205953831464,100.0,0.0,22.97466,1.153527213809888,1874.9847399999999,0.0,22.97466,19.402582216262818,1.9577010142186166,0.0,0.0,0.0
T-ALNS,1893.51538,62.880952004856034,100.0,0.0,23.124559999999995,1.2951313612912023,1870.39082,0.0,23.124559999999995,17.40975332260132,1.1775580697211723,0.0,0.0,0.0
T-ALNS-RRD,1892.75974,58.436822595286664,99.57446808510639,0.9515182882977844,23.177039999999998,1.2170745018280524,1869.5270800000003,0.05561999999999898,23.177039999999998,16.82726149559021,1.2918458665987367,0.05561999999999898,0.05561999999999898,0.2
1 algorithm total_cost_mean total_cost_std otdr_mean otdr_std ces_mean ces_std travel_time_mean delay_penalty_mean congestion_cost_mean computation_time_mean computation_time_std avg_delay_mean max_delay_mean late_customers_mean
2 Static-VRPTW 7580.3396999999995 1.016845989170083e-12 68.08510638297872 0.0 30.313699999999994 3.972054645195637e-15 3044.2300999999998 4505.795900000001 30.313699999999994 0.004783535003662109 0.00018088214390829616 300.3863933333334 493.0151000000001 15.0
3 TA-VRPTW-Greedy 2217.5418 7.227591305891663 100.0 0.0 22.31862 0.32531771700907924 2195.22318 0.0 22.31862 0.028783178329467772 0.002923092418020413 0.0 0.0 0.0
4 ALNS-Base 1897.9593999999997 64.64205953831464 100.0 0.0 22.97466 1.153527213809888 1874.9847399999999 0.0 22.97466 19.402582216262818 1.9577010142186166 0.0 0.0 0.0
5 T-ALNS 1893.51538 62.880952004856034 100.0 0.0 23.124559999999995 1.2951313612912023 1870.39082 0.0 23.124559999999995 17.40975332260132 1.1775580697211723 0.0 0.0 0.0
6 T-ALNS-RRD 1892.75974 58.436822595286664 99.57446808510639 0.9515182882977844 23.177039999999998 1.2170745018280524 1869.5270800000003 0.05561999999999898 23.177039999999998 16.82726149559021 1.2918458665987367 0.05561999999999898 0.05561999999999898 0.2

View File

@@ -0,0 +1,3 @@
"""
T-ALNS-RRD Reproduction Package.
"""

View File

@@ -0,0 +1,62 @@
"""
Simulated Annealing acceptance criterion (paper Eq.20-21).
P_accept = 1 if f(S') < f(S)
= exp(-Δf / τ_t) otherwise
τ_{t+1} = γ × τ_t, γ ∈ (0, 1)
"""
import numpy as np
class SimulatedAnnealing:
"""Simulated annealing acceptance criterion with geometric cooling."""
def __init__(
self,
initial_temp: float = 500.0,
cooling_rate: float = 0.99975,
min_temp: float = 0.1,
seed: int = None,
):
self.initial_temp = initial_temp
self.cooling_rate = cooling_rate
self.min_temp = min_temp
self._temp = initial_temp
self._rng = np.random.default_rng(seed)
@property
def temperature(self) -> float:
return self._temp
def accept(self, current_cost: float, new_cost: float) -> bool:
"""Decide whether to accept the new solution (Eq.20).
Returns True if new solution should be accepted.
"""
if new_cost < current_cost:
return True
delta = new_cost - current_cost
# Avoid numerical issues
if delta <= 0:
return True
prob = np.exp(-delta / max(self._temp, 1e-10))
return self._rng.random() < prob
def cool(self):
"""Apply geometric cooling (Eq.21)."""
self._temp = max(self._temp * self.cooling_rate, self.min_temp)
def cool_to(self, iteration: int):
"""Cool by iteration count: τ_t = τ₀ × γ^t."""
self._temp = max(
self.initial_temp * (self.cooling_rate ** iteration),
self.min_temp,
)
def reset(self):
"""Reset temperature to initial value."""
self._temp = self.initial_temp

View File

@@ -0,0 +1,255 @@
"""
ALNS-Base: Adaptive Large Neighborhood Search (Algorithm 1).
Core optimization engine using destroy-repair cycles with adaptive
operator selection and simulated annealing acceptance.
"""
import time
import numpy as np
from typing import Dict, List, Optional, Tuple
from ..problem import Solution, ProblemContext
from ..cost import CostCalculator
from .operators_destroy import destroy_random, destroy_worst, destroy_related
from .operators_repair import repair_greedy, repair_regret2, repair_time_window_aware
from .acceptance import SimulatedAnnealing
class ALNSBase:
"""Adaptive Large Neighborhood Search - Baseline (no Tabu, no RRD).
Implements Algorithm 1 from the paper.
"""
def __init__(
self,
problem_ctx: ProblemContext,
cost_calc: CostCalculator,
config: dict = None,
):
self.ctx = problem_ctx
self.cost_calc = cost_calc
# Default config
default_cfg = {
"max_iterations": 1000,
"time_limit_sec": 600,
"destroy_ratio_min": 0.1,
"destroy_ratio_max": 0.4,
"initial_temperature_factor": 0.05,
"cooling_rate": 0.99975,
"reaction_factor": 0.1,
"segment_length": 100,
"stall_limit": 200,
"max_attempts": 5,
"reward_global_best": 1.0,
"reward_improvement": 0.5,
"reward_accepted": 0.2,
"reward_rejected": 0.0,
}
if config:
default_cfg.update(config)
self.cfg = default_cfg
# Register operators
self.destroy_ops = {
"random": destroy_random,
"worst": destroy_worst,
"related": destroy_related,
}
self.repair_ops = {
"greedy": repair_greedy,
"regret2": repair_regret2,
"time_window": repair_time_window_aware,
}
# Adaptive weights
self.destroy_weights: Dict[str, float] = {}
self.repair_weights: Dict[str, float] = {}
# Stats tracking
self.iteration_history: List[float] = []
self.best_cost_history: List[float] = []
def _construct_initial(self, seed: int = None) -> Solution:
"""Build initial solution using greedy insertion with traffic-aware costs."""
rng = np.random.default_rng(seed)
solution = Solution(self.ctx.n_vehicles)
customer_ids = list(self.ctx.customers.keys())
rng.shuffle(customer_ids)
for cid in customer_ids:
cust = self.ctx.customers[cid]
best_route = -1
best_pos = -1
best_cost = float("inf")
for k, route in enumerate(solution.routes):
if route.total_demand(self.ctx.customers) + cust.demand_kg > self.ctx.vehicle_capacity:
continue
pos, cost = self.cost_calc.find_best_insertion(
route, cid, self.ctx, use_full=True
)
if cost < best_cost:
best_cost = cost
best_route = k
best_pos = pos
if best_route >= 0:
solution.routes[best_route].insert(cid, best_pos)
return solution
def _select_operator(
self,
weights: Dict[str, float],
rng: np.random.Generator,
) -> str:
"""Roulette wheel selection based on operator weights (Eq.18)."""
names = list(weights.keys())
w = np.array([weights[n] for n in names])
total = w.sum()
if total <= 0:
return rng.choice(names)
probs = w / total
return rng.choice(names, p=probs)
def _update_weights(
self,
d_name: str,
r_name: str,
reward: float,
):
"""Update operator weights with reaction factor (Eq.19).
ω_h = (1 - ξ) × ω_h + ξ × θ_r
"""
xi = self.cfg["reaction_factor"]
self.destroy_weights[d_name] = (
1 - xi
) * self.destroy_weights.get(d_name, 1.0) + xi * reward
self.repair_weights[r_name] = (
1 - xi
) * self.repair_weights.get(r_name, 1.0) + xi * reward
def solve(self, seed: int = None) -> Solution:
"""Run the ALNS optimization loop (Algorithm 1).
Returns the best solution found.
"""
rng = np.random.default_rng(seed)
t_start = time.time()
# Initialize weights
self.destroy_weights = {name: 1.0 for name in self.destroy_ops}
self.repair_weights = {name: 1.0 for name in self.repair_ops}
# Build initial solution
S_current = self._construct_initial(seed=seed)
S_best = S_current.copy()
current_cost = self.cost_calc.compute_total_cost(S_current, self.ctx)
best_cost = current_cost
# Initialize SA
initial_temp = self.cfg["initial_temperature_factor"] * max(current_cost, 1.0)
sa = SimulatedAnnealing(
initial_temp=initial_temp,
cooling_rate=self.cfg["cooling_rate"],
seed=seed,
)
n_customers = len(self.ctx.customers)
q_min = max(1, int(self.cfg["destroy_ratio_min"] * n_customers))
q_max = max(q_min + 1, int(self.cfg["destroy_ratio_max"] * n_customers))
stall_counter = 0
iter_count = 0
self.iteration_history = []
self.best_cost_history = []
# Main loop
while iter_count < self.cfg["max_iterations"]:
elapsed = time.time() - t_start
if elapsed > self.cfg["time_limit_sec"]:
break
if stall_counter >= self.cfg["stall_limit"]:
break
# Select operators
for attempt in range(self.cfg["max_attempts"]):
d_name = self._select_operator(self.destroy_weights, rng)
r_name = self._select_operator(self.repair_weights, rng)
q = rng.integers(q_min, q_max + 1)
# Apply destroy
S_temp = S_current.copy()
if d_name == "worst":
S_temp, removed = self.destroy_ops[d_name](
S_temp, self.ctx, self.cost_calc, rng, q
)
elif d_name == "related":
S_temp, removed = self.destroy_ops[d_name](
S_temp, self.ctx, rng, q
)
else:
S_temp, removed = self.destroy_ops[d_name](S_temp, rng, q)
if not removed:
continue
# Apply repair
S_new = self.repair_ops[r_name](
S_temp, removed, self.ctx, self.cost_calc, rng
)
new_cost = self.cost_calc.compute_total_cost(S_new, self.ctx)
# Acceptance decision (Eq.20)
if sa.accept(current_cost, new_cost):
S_current = S_new
current_cost = new_cost
if new_cost < best_cost:
S_best = S_new.copy()
best_cost = new_cost
stall_counter = 0
reward = self.cfg["reward_global_best"]
else:
stall_counter += 1
reward = self.cfg["reward_improvement"]
else:
reward = self.cfg["reward_rejected"]
# Update weights every π iterations
if iter_count % self.cfg["segment_length"] == 0:
self._update_weights(d_name, r_name, reward)
break # exit attempt loop after successful operator application
# Cool temperature
sa.cool()
# Record history
self.iteration_history.append(current_cost)
self.best_cost_history.append(best_cost)
iter_count += 1
self._iter_count = iter_count
self._best_cost = best_cost
return S_best
def run(self, seed: int = None) -> dict:
"""Run solver and return comprehensive metrics."""
t0 = time.time()
solution = self.solve(seed=seed)
elapsed = time.time() - t0
metrics = self.cost_calc.evaluate_solution(solution, self.ctx)
metrics["computation_time"] = elapsed
metrics["algorithm"] = "ALNS-Base"
metrics["iterations"] = getattr(self, "_iter_count", 0)
metrics["convergence_history"] = self.best_cost_history
metrics["solution"] = solution # Include the Solution object for visualization
return metrics

View File

@@ -0,0 +1,192 @@
"""
ALNS Destroy Operators (paper §3.3.1).
Three destruction strategies: random, worst, and relatedness removal.
Each removes a subset of customers from the current solution.
"""
import numpy as np
from typing import List, Tuple
from ..problem import Solution
def destroy_random(
solution: Solution,
rng: np.random.Generator,
n_remove: int,
) -> Tuple[Solution, List[int]]:
"""Random removal: uniformly select n_remove customers.
Args:
solution: Current solution to destroy.
rng: Seeded random generator.
n_remove: Number of customers to remove.
Returns:
(destroyed_solution, list_of_removed_customer_ids)
"""
new_sol = solution.copy()
all_customers = []
route_indices = []
for k, route in enumerate(new_sol.routes):
for c in route.customers:
all_customers.append(c)
route_indices.append(k)
if len(all_customers) == 0:
return new_sol, []
n_remove = min(n_remove, len(all_customers))
remove_indices = rng.choice(len(all_customers), size=n_remove, replace=False)
removed = [all_customers[i] for i in remove_indices]
for c in removed:
route_idx = new_sol.find_route(c)
if route_idx is not None:
new_sol.routes[route_idx].remove(c)
return new_sol, removed
def destroy_worst(
solution: Solution,
problem_ctx: "ProblemContext",
cost_calc: "CostCalculator",
rng: np.random.Generator,
n_remove: int,
) -> Tuple[Solution, List[int]]:
"""Worst removal: remove customers with highest cost contribution.
For each customer, compute the cost delta if removed (by evaluating
the route with and without the customer). Remove those with highest cost.
Uses randomized worst: picks from the top candidates with some noise.
"""
new_sol = solution.copy()
customers = problem_ctx.customers
# Compute cost for each customer in current position
cost_contributions = []
for k, route in enumerate(new_sol.routes):
for c in route.customers:
# Approximate cost contribution: insertion cost of this customer
# We use full route evaluation with and without customer
test_route = route.copy()
test_route.remove(c)
cost_with = cost_calc.propagate_route(route.nodes, problem_ctx)
cost_without = cost_calc.propagate_route(test_route.nodes, problem_ctx)
delta = (
(cost_with["total_travel_time"] - cost_without["total_travel_time"])
+ cost_calc.lambda_lateness * (cost_with["total_delay"] - cost_without["total_delay"])
+ cost_calc.lambda_congestion * (cost_with["congestion_exposure"] - cost_without["congestion_exposure"])
)
cost_contributions.append((c, k, max(0, delta)))
if not cost_contributions:
return new_sol, []
# Sort by cost contribution (descending)
cost_contributions.sort(key=lambda x: x[2], reverse=True)
# Randomized selection: pick from top 2*n_remove with probability weighted by cost
pool_size = min(len(cost_contributions), max(n_remove, 2 * n_remove))
pool = cost_contributions[:pool_size]
weights = np.array([x[2] + 1.0 for x in pool])
weights /= weights.sum()
n_remove = min(n_remove, len(pool))
selected_indices = rng.choice(len(pool), size=n_remove, replace=False, p=weights)
removed = [pool[i][0] for i in selected_indices]
for c in removed:
route_idx = new_sol.find_route(c)
if route_idx is not None:
new_sol.routes[route_idx].remove(c)
return new_sol, removed
def destroy_related(
solution: Solution,
problem_ctx: "ProblemContext",
rng: np.random.Generator,
n_remove: int,
) -> Tuple[Solution, List[int]]:
"""Shaw removal: remove customers that are related to each other.
1. Pick a random seed customer.
2. Compute relatedness to all other customers.
3. Iteratively remove the most related customer.
4. Repeat until n_remove customers removed.
Relatedness = distance / max_distance + |tw_center_diff| / max_tw_diff
Lower relatedness score = more related.
"""
new_sol = solution.copy()
customers = problem_ctx.customers
# Gather all assigned customers with their positions
all_custs = []
for route in new_sol.routes:
for c in route.customers:
all_custs.append(c)
if not all_custs:
return new_sol, []
n_remove = min(n_remove, len(all_custs))
# Pick seed customer randomly
seed_cust = rng.choice(all_custs)
removed = [seed_cust]
remaining = set(all_custs) - {seed_cust}
# Precompute max values for normalization
max_dist = np.sqrt(problem_ctx.depot.x_km**2 + problem_ctx.depot.y_km**2) * 2
max_tw = (problem_ctx.op_end - problem_ctx.op_start)
while len(removed) < n_remove and remaining:
# Find customer most related to ANY already-removed customer
best_cust = None
best_rel = float("inf")
for c in remaining:
c_obj = customers[c]
# Compute min relatedness to any removed customer
min_rel = float("inf")
for r_c in removed:
r_obj = customers[r_c]
# Spatial relatedness
dist = np.sqrt(
(c_obj.x_km - r_obj.x_km) ** 2 + (c_obj.y_km - r_obj.y_km) ** 2
)
# Time window relatedness
tw_c = (c_obj.earliest_time_min + c_obj.latest_time_min) / 2
tw_r = (r_obj.earliest_time_min + r_obj.latest_time_min) / 2
tw_diff = abs(tw_c - tw_r)
rel = (dist / max_dist) + (tw_diff / max_tw)
if rel < min_rel:
min_rel = rel
if min_rel < best_rel:
best_rel = min_rel
best_cust = c
if best_cust is not None:
removed.append(best_cust)
remaining.remove(best_cust)
else:
# Fallback: pick random
best_cust = rng.choice(list(remaining))
removed.append(best_cust)
remaining.remove(best_cust)
# Apply removals
for c in removed:
route_idx = new_sol.find_route(c)
if route_idx is not None:
new_sol.routes[route_idx].remove(c)
return new_sol, removed

View File

@@ -0,0 +1,151 @@
"""
ALNS Repair Operators (paper §3.3.1).
Three repair strategies: greedy, regret-2, and time-window-aware insertion.
Each reinserts removed customers back into the solution.
"""
import numpy as np
from typing import List
from ..problem import Solution
from ..cost import CostCalculator
def repair_greedy(
solution: Solution,
removed_customers: List[int],
problem_ctx: "ProblemContext",
cost_calc: CostCalculator,
rng: np.random.Generator,
) -> Solution:
"""Greedy insertion: for each removed customer, find cheapest feasible
position across all routes. Insert in random order.
Returns the repaired solution (in-place modification).
"""
customers = problem_ctx.customers
# Randomize insertion order
order = list(removed_customers)
rng.shuffle(order)
for cid in order:
cust = customers[cid]
best_route = -1
best_pos = -1
best_cost = float("inf")
for k, route in enumerate(solution.routes):
if route.total_demand(customers) + cust.demand_kg > problem_ctx.vehicle_capacity:
continue
pos, cost = cost_calc.find_best_insertion(route, cid, problem_ctx, use_full=True)
if cost < best_cost:
best_cost = cost
best_route = k
best_pos = pos
if best_route >= 0:
solution.routes[best_route].insert(cid, best_pos)
return solution
def repair_regret2(
solution: Solution,
removed_customers: List[int],
problem_ctx: "ProblemContext",
cost_calc: CostCalculator,
rng: np.random.Generator,
) -> Solution:
"""Regret-2 insertion: insert customer with largest gap between
best and 2nd-best insertion position first.
Repeat until all customers inserted.
"""
customers = problem_ctx.customers
remaining = set(removed_customers)
while remaining:
best_cid = None
best_route = -1
best_pos = -1
best_regret = -float("inf")
for cid in list(remaining):
cust = customers[cid]
route_costs = [] # (route_idx, position, cost)
for k, route in enumerate(solution.routes):
if route.total_demand(customers) + cust.demand_kg > problem_ctx.vehicle_capacity:
continue
pos, cost = cost_calc.find_best_insertion(route, cid, problem_ctx, use_full=True)
route_costs.append((k, pos, cost))
if not route_costs:
continue
route_costs.sort(key=lambda x: x[2])
best = route_costs[0][2]
if len(route_costs) > 1:
regret = route_costs[1][2] - best
else:
regret = float("inf") # Only one option, insert immediately
if regret > best_regret:
best_regret = regret
best_cid = cid
best_route = route_costs[0][0]
best_pos = route_costs[0][1]
if best_cid is not None and best_route >= 0:
solution.routes[best_route].insert(best_cid, best_pos)
remaining.remove(best_cid)
else:
# No feasible insertion found, try greedy as fallback
if remaining:
cid = remaining.pop()
for k, route in enumerate(solution.routes):
if route.total_demand(customers) + customers[cid].demand_kg <= problem_ctx.vehicle_capacity:
pos, _ = cost_calc.find_best_insertion(route, cid, problem_ctx, use_full=True)
solution.routes[k].insert(cid, pos)
break
return solution
def repair_time_window_aware(
solution: Solution,
removed_customers: List[int],
problem_ctx: "ProblemContext",
cost_calc: CostCalculator,
rng: np.random.Generator,
) -> Solution:
"""Time-window-aware insertion: prioritize customers with tightest
time windows. Sort by window length, insert tightest first.
"""
customers = problem_ctx.customers
# Sort by time window length (tightest first)
order = sorted(
removed_customers,
key=lambda cid: customers[cid].latest_time_min - customers[cid].earliest_time_min,
)
for cid in order:
cust = customers[cid]
best_route = -1
best_pos = -1
best_cost = float("inf")
for k, route in enumerate(solution.routes):
if route.total_demand(customers) + cust.demand_kg > problem_ctx.vehicle_capacity:
continue
pos, cost = cost_calc.find_best_insertion(route, cid, problem_ctx, use_full=True)
if cost < best_cost:
best_cost = cost
best_route = k
best_pos = pos
if best_route >= 0:
solution.routes[best_route].insert(cid, best_pos)
return solution

View File

@@ -0,0 +1,122 @@
"""
Static-VRPTW: Greedy insertion baseline WITHOUT time-dependent travel
times or congestion penalties. Uses fixed base travel times only.
"""
import time
import numpy as np
from typing import Dict, Optional
from ..problem import Solution, ProblemContext, Route
from ..cost import CostCalculator
class StaticVRPTWSolver:
"""Static Vehicle Routing Problem with Time Windows - greedy baseline.
Uses fixed travel times (no time dependency, no congestion).
"""
def __init__(self, problem_ctx: ProblemContext, cost_calc: CostCalculator):
self.ctx = problem_ctx
self.cost_calc = cost_calc
def solve(self, seed: int = None) -> Solution:
"""Build a solution using greedy insertion with static travel times."""
rng = np.random.default_rng(seed)
solution = Solution(self.ctx.n_vehicles)
# Get customers sorted by earliest time
customer_ids = sorted(
list(self.ctx.customers.keys()),
key=lambda cid: self.ctx.customers[cid].earliest_time_min,
)
for cid in customer_ids:
best_route_idx = -1
best_position = -1
best_cost = float("inf")
# Try inserting into each vehicle's route
for k, route in enumerate(solution.routes):
cust = self.ctx.customers[cid]
# Check capacity
if route.total_demand(self.ctx.customers) + cust.demand_kg > self.ctx.vehicle_capacity:
continue
pos, cost = self._find_best_static_insertion(route, cid, rng)
if cost < best_cost:
best_cost = cost
best_route_idx = k
best_position = pos
if best_route_idx >= 0:
solution.routes[best_route_idx].insert(cid, best_position)
# else: customer cannot be assigned (capacity exhausted)
return solution
def _find_best_static_insertion(self, route: Route, customer_id: int,
rng: np.random.Generator) -> tuple:
"""Find cheapest position to insert customer using STATIC travel times.
Static time: use traffic.travel_time[i, j, 0] (first interval, no time dependency).
"""
nodes = route.nodes
customers = self.ctx.customers
cust = customers[customer_id]
n_cust = len(route.customers)
best_pos = 1
best_cost = float("inf")
for pos in range(1, n_cust + 2):
# Build candidate route
candidate = list(nodes)
candidate.insert(pos, customer_id)
# Compute static cost (no λ terms, no congestion)
cost = self._static_route_cost(candidate)
if cost < best_cost:
best_cost = cost
best_pos = pos
return best_pos, best_cost
def _static_route_cost(self, nodes: list) -> float:
"""Compute total travel time using average travel time over all intervals.
Uses mean travel time across all 12 time intervals per arc,
NOT just interval 0. This provides a fair baseline that doesn't
artificially benefit from picking off-peak times.
"""
total = 0.0
depart_time = self.ctx.op_start
for idx in range(1, len(nodes)):
i, j = nodes[idx - 1], nodes[idx]
vals = []
for h in range(self.ctx.n_intervals):
v = self.ctx.traffic.travel_time[i, j, h]
if not np.isinf(v):
vals.append(v)
tt = np.mean(vals) if vals else 0.0
total += tt
depart_time += tt
if j != 0:
depart_time += self.ctx.customers[j].service_time_min
return total
def run(self, seed: int = None) -> dict:
"""Run solver and return metrics.
Evaluates using the full cost function for fair comparison.
Static constructs routes using average travel times (not just off-peak)
but is evaluated under the same time-dependent conditions as all algorithms.
"""
t0 = time.time()
solution = self.solve(seed=seed)
elapsed = time.time() - t0
metrics = self.cost_calc.evaluate_solution(solution, self.ctx)
metrics["computation_time"] = elapsed
metrics["algorithm"] = "Static-VRPTW"
return metrics

View File

@@ -0,0 +1,68 @@
"""
TA-VRPTW-Greedy: Traffic-Aware greedy insertion baseline.
Uses time-dependent travel times and congestion penalties,
but without metaheuristic search (no ALNS).
"""
import time
import numpy as np
from typing import Dict, Optional
from ..problem import Solution, ProblemContext, Route
from ..cost import CostCalculator
class TAGreedySolver:
"""Traffic-Aware VRPTW - greedy baseline with time-dependent costs.
Uses t_ij(T_i) and ρ_ij(T_i) but greedy insertion only.
"""
def __init__(self, problem_ctx: ProblemContext, cost_calc: CostCalculator):
self.ctx = problem_ctx
self.cost_calc = cost_calc
def solve(self, seed: int = None) -> Solution:
"""Build solution using greedy insertion with traffic-aware costs."""
rng = np.random.default_rng(seed)
solution = Solution(self.ctx.n_vehicles)
customer_ids = list(self.ctx.customers.keys())
# Randomize order for unbiased construction
rng.shuffle(customer_ids)
for cid in customer_ids:
best_route_idx = -1
best_position = -1
best_cost = float("inf")
for k, route in enumerate(solution.routes):
cust = self.ctx.customers[cid]
# Capacity check
if route.total_demand(self.ctx.customers) + cust.demand_kg > self.ctx.vehicle_capacity:
continue
n_cust = len(route.customers)
for pos in range(1, n_cust + 2):
cost = self.cost_calc.compute_insertion_cost_full(
route, cid, pos, self.ctx
)
if cost < best_cost:
best_cost = cost
best_route_idx = k
best_position = pos
if best_route_idx >= 0:
solution.routes[best_route_idx].insert(cid, best_position)
return solution
def run(self, seed: int = None) -> dict:
"""Run solver and return metrics."""
t0 = time.time()
solution = self.solve(seed=seed)
elapsed = time.time() - t0
metrics = self.cost_calc.evaluate_solution(solution, self.ctx)
metrics["computation_time"] = elapsed
metrics["algorithm"] = "TA-VRPTW-Greedy"
return metrics

View File

@@ -0,0 +1,429 @@
"""
Cost functions for the DVRPTW-TA problem.
Implements the composite objective (Eq.1) and insertion cost evaluation
(Eq.16-17) from the paper.
"""
from typing import Dict, List, Tuple
import numpy as np
class CostCalculator:
"""Computes costs for routing solutions under traffic-aware conditions."""
def __init__(
self,
lambda_lateness: float = 1.0,
lambda_congestion: float = 1.0,
lambda_stability: float = 0.3,
):
self.lambda_lateness = lambda_lateness
self.lambda_congestion = lambda_congestion
self.lambda_stability = lambda_stability
# ------------------------------------------------------------------
# Route-level time propagation
# ------------------------------------------------------------------
def propagate_route(
self,
route_nodes: List[int],
problem_ctx: "ProblemContext",
depot_start_time: float = 360.0,
) -> dict:
"""Compute all time values along a route.
Implements Eq.5:
A_j = T_i + t_ij(T_i)
S_j = max(A_j, e_j)
T_j = S_j + s_j
δ_j = max(0, S_j - l_j)
Returns dict with arrival_times, service_starts, departures,
delays, wait_times, and totals.
"""
customers = problem_ctx.customers
n = len(route_nodes)
arrivals = [0.0] * n
service_starts = [0.0] * n
departures = [0.0] * n
delays = [0.0] * n
waits = [0.0] * n
congestion_exposure = 0.0
# First node (depot)
departures[0] = depot_start_time
service_starts[0] = depot_start_time
arrivals[0] = depot_start_time
for idx in range(1, n):
i = route_nodes[idx - 1]
j = route_nodes[idx]
# Arrival at j (Eq.5)
arrivals[idx] = departures[idx - 1] + problem_ctx.get_travel_time(
i, j, departures[idx - 1]
)
# Accumulate congestion exposure
congestion_exposure += problem_ctx.get_congestion_penalty(
i, j, departures[idx - 1]
)
# Service start (wait if early)
if j == 0:
# Return to depot: no service time, no time window
service_starts[idx] = arrivals[idx]
delays[idx] = 0.0
waits[idx] = 0.0
else:
cust = customers.get(j)
if cust is None:
# Should not happen in valid solutions
service_starts[idx] = arrivals[idx]
delays[idx] = 0.0
waits[idx] = 0.0
else:
# Soft time window: wait if early, record delay if late
service_starts[idx] = max(arrivals[idx], cust.earliest_time_min)
waits[idx] = max(0.0, cust.earliest_time_min - arrivals[idx])
delays[idx] = max(0.0, service_starts[idx] - cust.latest_time_min)
# Departure (Eq.5)
if j == 0:
departures[idx] = arrivals[idx]
else:
service_time = customers[j].service_time_min if j in customers else 0.0
departures[idx] = service_starts[idx] + service_time
return {
"arrivals": arrivals,
"service_starts": service_starts,
"departures": departures,
"delays": delays,
"waits": waits,
"total_travel_time": arrivals[-1] - depot_start_time,
"total_delay": sum(delays),
"total_wait": sum(waits),
"congestion_exposure": congestion_exposure,
}
# ------------------------------------------------------------------
# Composite cost (Eq.1)
# ------------------------------------------------------------------
def compute_total_cost(
self,
solution: "Solution",
problem_ctx: "ProblemContext",
previous_solution: "Solution" = None,
) -> float:
"""Compute composite objective value per Eq.1.
Cost = Σ_k Σ_(i,j) [t_ij(T_i) + λ₂·ρ_ij(T_i)] + λ₁·Σ_j δ_j
For RRD: also includes λ₃ × stability penalty.
"""
total_travel_time = 0.0
total_delay = 0.0
total_congestion = 0.0
for route in solution.routes:
if len(route.nodes) < 2:
continue
result = self.propagate_route(route.nodes, problem_ctx)
# Travel time: sum of all arc travel times
total_travel_time += result["total_travel_time"]
# Delay penalty: sum of lateness at all customers
total_delay += result["total_delay"]
# Congestion: sum of ρ_ij on traversed arcs
total_congestion += result["congestion_exposure"]
cost = (
total_travel_time
+ self.lambda_lateness * total_delay
+ self.lambda_congestion * total_congestion
)
# Stability penalty (RRD only, when comparing to previous solution)
if previous_solution is not None:
stability = self._compute_stability(solution, previous_solution)
cost += self.lambda_stability * stability
return cost
def compute_travel_time_cost(
self, solution: "Solution", problem_ctx: "ProblemContext"
) -> float:
total = 0.0
for route in solution.routes:
if len(route.nodes) < 2:
continue
for idx in range(1, len(route.nodes)):
i, j = route.nodes[idx - 1], route.nodes[idx]
depart = self._get_departure_at(route.nodes, idx - 1, problem_ctx)
total += problem_ctx.get_travel_time(i, j, depart)
return total
def compute_lateness_penalty(
self, solution: "Solution", problem_ctx: "ProblemContext"
) -> float:
total = 0.0
for route in solution.routes:
result = self.propagate_route(route.nodes, problem_ctx)
total += result["total_delay"]
return self.lambda_lateness * total
def compute_congestion_exposure(
self, solution: "Solution", problem_ctx: "ProblemContext"
) -> float:
total = 0.0
for route in solution.routes:
result = self.propagate_route(route.nodes, problem_ctx)
total += result["congestion_exposure"]
return total
# ------------------------------------------------------------------
# Insertion cost (Eq.16-17)
# ------------------------------------------------------------------
def compute_insertion_cost(
self,
route: "Route",
customer_id: int,
position: int,
problem_ctx: "ProblemContext",
) -> float:
"""Compute marginal cost of inserting a customer at a position.
Eq.16: Δf_ijk = t_ji(T_j) + s_i + t_ik(T_j + t_ji + s_i) - t_jk(T_j)
+ λ₁·δ_i(T_i) + λ₂·ρ_ji(T_j)
This computes only the local change; a full suffix propagation
(Eq.17) would recompute downstream times, which is done in
compute_insertion_cost_full().
"""
nodes = route.nodes
customers = problem_ctx.customers
cust = customers[customer_id]
# Find predecessor (j) and successor (k) in the route
# Position is 1-based among customers
if position <= len(route.customers):
# Insert between existing nodes
insert_idx = position # position in nodes list
j = nodes[insert_idx - 1]
k = nodes[insert_idx]
else:
# Insert before returning to depot
j = nodes[-2] # last customer before depot
k = nodes[-1] # depot (0)
# Get departure time from j
depart_j = self._get_departure_at(nodes, nodes.index(j), problem_ctx)
# Old cost: t_jk(T_j)
old_cost = problem_ctx.get_travel_time(j, k, depart_j)
# New cost: t_ji(T_j) + s_i + t_ik(T_j + t_ji + s_i)
t_ji = problem_ctx.get_travel_time(j, customer_id, depart_j)
arrival_i = depart_j + t_ji
service_start_i = max(arrival_i, cust.earliest_time_min)
t_ik = problem_ctx.get_travel_time(
customer_id, k, service_start_i + cust.service_time_min
)
travel_component = t_ji + cust.service_time_min + t_ik - old_cost
# Delay penalty at i (λ₁·δ_i)
lateness_i = max(0.0, service_start_i - cust.latest_time_min)
delay_penalty = self.lambda_lateness * lateness_i
# Congestion penalty for new arc (j,i) (λ₂·ρ_ji)
congestion_penalty = self.lambda_congestion * problem_ctx.get_congestion_penalty(
j, customer_id, depart_j
)
return travel_component + delay_penalty + congestion_penalty
def compute_insertion_cost_full(
self,
route: "Route",
customer_id: int,
position: int,
problem_ctx: "ProblemContext",
) -> float:
"""Full insertion cost with suffix propagation (Eq.17).
Evaluates the complete impact of insertion including all
downstream time changes due to FIFO shift.
"""
# Build candidate route with customer inserted
candidate_nodes = list(route.nodes)
# position is 1-based among customers
insert_at = position
candidate_nodes.insert(insert_at, customer_id)
# Evaluate old route cost
old_result = self.propagate_route(route.nodes, problem_ctx)
old_cost = (
old_result["total_travel_time"]
+ self.lambda_lateness * old_result["total_delay"]
+ self.lambda_congestion * old_result["congestion_exposure"]
)
# Evaluate new route cost (full propagation)
new_result = self.propagate_route(candidate_nodes, problem_ctx)
new_cost = (
new_result["total_travel_time"]
+ self.lambda_lateness * new_result["total_delay"]
+ self.lambda_congestion * new_result["congestion_exposure"]
)
return new_cost - old_cost
def find_best_insertion(
self,
route: "Route",
customer_id: int,
problem_ctx: "ProblemContext",
use_full: bool = True,
) -> Tuple[int, float]:
"""Find best position to insert a customer into a route.
Returns (best_position, best_cost).
position is 1-based among customers (1 = first customer, n+1 = last).
"""
n_cust = len(route.customers)
best_pos = 1
best_cost = float("inf")
cost_fn = self.compute_insertion_cost_full if use_full else self.compute_insertion_cost
for pos in range(1, n_cust + 2):
cost = cost_fn(route, customer_id, pos, problem_ctx)
if cost < best_cost:
best_cost = cost
best_pos = pos
return best_pos, best_cost
# ------------------------------------------------------------------
# Stability and helpers
# ------------------------------------------------------------------
def _compute_stability(
self, new_sol: "Solution", old_sol: "Solution"
) -> float:
"""Compute route stability penalty (Eq.41).
Stability = Σ_k |R^a_k ∩ R^s_k| / |R^a_k R^s_k|
Higher = more stable. We invert for penalty.
"""
stability = 0.0
for k in range(min(new_sol.n_vehicles, old_sol.n_vehicles)):
new_set = set(new_sol.routes[k].customers)
old_set = set(old_sol.routes[k].customers)
union = new_set | old_set
inter = new_set & old_set
if len(union) > 0:
stability += len(inter) / len(union)
# Penalize route changes (invert: more stable = lower penalty)
return (new_sol.n_vehicles - stability) * 100.0
def _get_departure_at(
self,
nodes: List[int],
idx: int,
problem_ctx: "ProblemContext",
start_time: float = 360.0,
) -> float:
"""Compute the departure time at a given position along a route."""
result = self.propagate_route(nodes, problem_ctx, start_time)
return result["departures"][idx]
# ------------------------------------------------------------------
# Solution quality metrics
# ------------------------------------------------------------------
def evaluate_solution(
self,
solution: "Solution",
problem_ctx: "ProblemContext",
include_penalties: bool = True,
) -> dict:
"""Comprehensive solution evaluation returning all metrics.
Args:
include_penalties: If False, total_cost = pure travel time only
(used for Static-VRPTW baseline which doesn't
account for congestion or time-dependent costs).
"""
total_travel = 0.0
total_delay = 0.0
total_congestion = 0.0
n_ontime = 0
n_late = 0
delays_list = []
max_delay = 0.0
avg_route_duration = 0.0
all_cust_ids = problem_ctx.customer_ids
assigned = solution.get_assignments()
for route in solution.routes:
if len(route.nodes) < 2:
continue
result = self.propagate_route(route.nodes, problem_ctx)
total_travel += result["total_travel_time"]
total_delay += result["total_delay"]
total_congestion += result["congestion_exposure"]
avg_route_duration += (
result["departures"][-1] - result["departures"][0]
)
# Count on-time deliveries
for idx, node in enumerate(route.nodes):
if node == 0:
continue
delay = result["delays"][idx]
if delay <= 0:
n_ontime += 1
else:
n_late += 1
delays_list.append(delay)
max_delay = max(max_delay, delay)
n_total = len(all_cust_ids)
otdr = n_ontime / n_total if n_total > 0 else 0.0
avg_delay = np.mean(delays_list) if delays_list else 0.0
avg_route_dur = (
avg_route_duration / solution.n_vehicles if solution.n_vehicles > 0 else 0.0
)
if include_penalties:
total_cost = (
total_travel
+ self.lambda_lateness * total_delay
+ self.lambda_congestion * total_congestion
)
else:
total_cost = total_travel
return {
"total_cost": total_cost,
"travel_time_cost": total_travel,
"delay_penalty": self.lambda_lateness * total_delay,
"congestion_cost": total_congestion,
"otdr": otdr,
"avg_delay": avg_delay,
"max_delay": max_delay,
"late_customers": n_late,
"ces": total_congestion,
"avg_route_duration": avg_route_dur,
"n_assigned": len(assigned),
"n_total": n_total,
}

View File

@@ -0,0 +1,470 @@
"""
Synthetic Data Generator for T-ALNS-RRD Reproduction.
Generates a city logistics dataset matching the paper's experimental setup:
- 47 customers, 1 depot, 4 vehicles
- 8 km x 10 km urban area
- Complete graph arc network
- Time-dependent traffic (12 one-hour intervals)
- Clustered customer spatial distribution
"""
import numpy as np
import pandas as pd
from pathlib import Path
from dataclasses import dataclass, field
from typing import Dict, List, Tuple, Optional
import yaml
@dataclass
class Customer:
"""A delivery customer with spatial, demand, and time window attributes."""
customer_id: int
x_km: float
y_km: float
demand_kg: float
service_time_min: float
earliest_time_min: float # e_i in minutes from midnight
latest_time_min: float # l_i in minutes from midnight
cluster: str = ""
@dataclass
class Depot:
"""Central warehouse depot (node 0)."""
depot_id: int = 0
x_km: float = 4.0 # center of 8x10 area
y_km: float = 5.0
@dataclass
class Arc:
"""A directed arc in the road network."""
from_node: int
to_node: int
distance_km: float
road_type: str # arterial, collector, residential
speed_kmh: float
base_travel_time_min: float
@dataclass
class TrafficData:
"""Spatiotemporal traffic tensors for all arcs and time intervals."""
n_nodes: int
n_intervals: int # H = 12
# Shape: (n_nodes, n_nodes, n_intervals)
travel_time: np.ndarray # t_ij^(h)
congestion: np.ndarray # γ_ij^(h) ∈ [0, 1]
uncertainty: np.ndarray # η_ij^(h)
congestion_penalty: np.ndarray # ρ_ij = θ × γ (Eq.9)
@property
def shape(self) -> Tuple[int, int, int]:
return self.travel_time.shape
class DataGenerator:
"""Generates the complete synthetic dataset for the reproduction experiment."""
def __init__(self, config_path: Optional[str] = None, seed: int = 42):
self.rng = np.random.default_rng(seed)
self.seed = seed
# Load config
if config_path is None:
config_path = Path(__file__).parent.parent / "configs" / "default.yaml"
with open(config_path) as f:
self.cfg = yaml.safe_load(f)
self._extract_params()
self._setup_road_types()
def _extract_params(self):
"""Extract key parameters from config."""
p = self.cfg["problem"]
self.n_customers = p["n_customers"]
self.n_vehicles = p["n_vehicles"]
self.vehicle_capacity = p["vehicle_capacity_kg"]
self.service_time = p["service_time_min"]
self.area_w = p["area_width_km"]
self.area_h = p["area_height_km"]
self.op_start = p["operating_start"] # minutes from midnight
self.op_end = p["operating_end"]
self.n_intervals = p["n_time_intervals"]
c = self.cfg["customers"]
self.demand_min = c["demand_min_kg"]
self.demand_max = c["demand_max_kg"]
self.tw_categories = c["time_window_categories"]
self.n_clusters = c["num_clusters"]
self.tw_window_min = c.get("window_length_min", 60)
self.tw_window_max = c.get("window_length_max", 150)
t = self.cfg["traffic"]
self.traffic_multipliers = np.array(t["multipliers"], dtype=np.float64)
self.congestion_scale = t["congestion_scale_theta"] # θ
self.risk_aversion = t["risk_aversion_beta"] # β
self.uncertainty_base = t["uncertainty_base"]
self.road_noise_std = self.cfg["roads"]["noise_std"]
def _setup_road_types(self):
"""Setup road type configurations."""
roads = self.cfg["roads"]["types"]
self.road_speeds = {}
self.road_proportions = []
self.road_names = []
for name, cfg in roads.items():
self.road_speeds[name] = cfg["speed_kmh"]
self.road_proportions.append(cfg["proportion"])
self.road_names.append(name)
self.road_proportions = np.array(self.road_proportions)
self.road_proportions /= self.road_proportions.sum()
# ------------------------------------------------------------------
# Customer Generation
# ------------------------------------------------------------------
def generate_customers(self) -> List[Customer]:
"""Generate 47 customers with clustered spatial distribution."""
customers = []
# Generate cluster centers
cluster_centers = self._generate_cluster_centers()
# Assign time window categories to clusters (cycling)
tw_keys = list(self.tw_categories.keys())
cluster_tw = [tw_keys[i % len(tw_keys)] for i in range(self.n_clusters)]
# Distribute customers across clusters
customers_per_cluster = self._distribute_customers()
customer_idx = 1
for cluster_id in range(self.n_clusters):
n_in_cluster = customers_per_cluster[cluster_id]
cx, cy = cluster_centers[cluster_id]
tw_name = cluster_tw[cluster_id]
tw_cfg = self.tw_categories[tw_name]
for _ in range(n_in_cluster):
# Position: Gaussian around cluster center
x = np.clip(cx + self.rng.normal(0, 0.8), 0.5, self.area_w - 0.5)
y = np.clip(cy + self.rng.normal(0, 0.8), 0.5, self.area_h - 0.5)
# Demand: uniform within range
demand = round(self.rng.uniform(self.demand_min, self.demand_max), 1)
# Time window: random within the category range
tw_length = tw_cfg["latest"] - tw_cfg["earliest"]
# Random start within first half of window
earliest = tw_cfg["earliest"] + self.rng.uniform(0, tw_length * 0.4)
# Window length: configurable range
window_len = self.rng.uniform(self.tw_window_min, self.tw_window_max)
latest = min(earliest + window_len, tw_cfg["latest"])
c = Customer(
customer_id=customer_idx,
x_km=round(x, 3),
y_km=round(y, 3),
demand_kg=demand,
service_time_min=self.service_time,
earliest_time_min=round(earliest),
latest_time_min=round(latest),
cluster=tw_name,
)
customers.append(c)
customer_idx += 1
# Shuffle so cluster order doesn't leak through IDs
self.rng.shuffle(customers)
for i, c in enumerate(customers):
c.customer_id = i + 1
return customers
def _generate_cluster_centers(self) -> List[Tuple[float, float]]:
"""Generate cluster centers spread across the area."""
centers = []
# Spread clusters across the area with some randomness
if self.n_clusters == 3:
# Residential, commercial, office clusters
centers = [
(self.area_w * 0.2, self.area_h * 0.3), # left-bottom: residential
(self.area_w * 0.65, self.area_h * 0.5), # right-center: commercial
(self.area_w * 0.4, self.area_h * 0.8), # center-top: office
]
else:
for i in range(self.n_clusters):
cx = self.rng.uniform(1.5, self.area_w - 1.5)
cy = self.rng.uniform(1.5, self.area_h - 1.5)
centers.append((cx, cy))
return centers
def _distribute_customers(self) -> List[int]:
"""Distribute N customers across clusters (not necessarily uniform)."""
# Base: uniform distribution
base = np.ones(self.n_clusters, dtype=int) * (self.n_customers // self.n_clusters)
remainder = self.n_customers - base.sum()
# Randomly assign remainder
extras = self.rng.choice(self.n_clusters, size=remainder, replace=False)
for e in extras:
base[e] += 1
return list(base)
# ------------------------------------------------------------------
# Arc / Road Network Generation
# ------------------------------------------------------------------
def generate_arcs(self, depot: Depot, customers: List[Customer]) -> List[Arc]:
"""Generate complete graph arcs with road types and travel properties."""
arcs = []
n_total = 1 + len(customers) # depot + customers
# Node 0 = depot, nodes 1..n_customers = customers
node_positions = {0: (depot.x_km, depot.y_km)}
for c in customers:
node_positions[c.customer_id] = (c.x_km, c.y_km)
arclist = []
for i in range(n_total):
for j in range(n_total):
if i == j:
continue
xi, yi = node_positions[i]
xj, yj = node_positions[j]
dist = np.sqrt((xi - xj) ** 2 + (yi - yj) ** 2)
arclist.append((i, j, dist))
# Assign road types based on node pair characteristics
road_assignments = self._assign_road_types(arclist, len(customers))
for (i, j, dist), road_type in zip(arclist, road_assignments):
speed = self.road_speeds[road_type]
base_time = dist / speed * 60.0 # convert to minutes
arcs.append(Arc(
from_node=i, to_node=j,
distance_km=round(dist, 4),
road_type=road_type,
speed_kmh=speed,
base_travel_time_min=round(base_time, 4),
))
return arcs
def _assign_road_types(self, arclist: List[Tuple], n_customers: int) -> List[str]:
"""Assign road types to arcs.
Strategy:
- Depot <-> customer: higher chance of arterial
- Customer <-> customer: depends on distance (short = residential, long = arterial)
- Random variation for realism
"""
assignments = []
for i, j, dist in arclist:
# Depot connections tend to be arterial/collector
if i == 0 or j == 0:
probs = [0.5, 0.35, 0.15] # arterial, collector, residential
else:
if dist < 1.5: # nearby customers
probs = [0.1, 0.3, 0.6] # mostly residential
elif dist < 4.0:
probs = [0.2, 0.5, 0.3] # mostly collector
else:
probs = [0.5, 0.4, 0.1] # mostly arterial
# Add noise
probs = np.array(probs) + self.rng.uniform(-0.05, 0.05, size=3)
probs = np.clip(probs, 0, 1)
probs /= probs.sum()
road_idx = self.rng.choice(len(self.road_names), p=probs)
assignments.append(self.road_names[road_idx])
return assignments
# ------------------------------------------------------------------
# Traffic Tensor Generation
# ------------------------------------------------------------------
def generate_traffic(self, arcs: List[Arc]) -> TrafficData:
"""Generate time-dependent traffic tensors.
For each arc (i,j) and time interval h, computes:
- travel_time[i,j,h]: time-dependent travel time (Eq.8)
- congestion[i,j,h]: normalized congestion weight γ ∈ [0,1]
- uncertainty[i,j,h]: travel time uncertainty margin η
- congestion_penalty[i,j,h]: ρ = θ × γ (Eq.9)
"""
n_nodes = 1 + self.n_customers
n_intervals = self.n_intervals
# Build lookup for arcs
arc_map = {}
for arc in arcs:
arc_map[(arc.from_node, arc.to_node)] = arc
travel_time = np.full((n_nodes, n_nodes, n_intervals), np.inf)
congestion = np.zeros((n_nodes, n_nodes, n_intervals))
uncertainty = np.zeros((n_nodes, n_nodes, n_intervals))
congestion_penalty = np.zeros((n_nodes, n_nodes, n_intervals))
for (i, j), arc in arc_map.items():
base_time = arc.base_travel_time_min
for h in range(n_intervals):
# Apply congestion multiplier with road-specific noise
noise = 1.0 + self.rng.normal(0, self.road_noise_std)
tt = base_time * self.traffic_multipliers[h] * noise
tt = max(tt, base_time * 0.5) # ensure non-negative and FIFO-compatible
# Congestion weight γ: proportional to how much > base time
gamma = min(1.0, max(0.0,
(self.traffic_multipliers[h] - 0.9) / 0.9
))
# Add spatial noise: arcs involving depot slightly less congested (better roads)
if i == 0 or j == 0:
gamma *= self.rng.uniform(0.7, 1.0)
# Uncertainty margin η
eta = tt * self.uncertainty_base * self.traffic_multipliers[h]
# Congestion penalty ρ (Eq.9): extra time caused by congestion
# ρ = θ × extra_time × γ, where extra_time = base_time × (multiplier - 1)
extra_time = base_time * max(0, self.traffic_multipliers[h] - 1.0)
rho = self.congestion_scale * extra_time * gamma
travel_time[i, j, h] = round(tt, 4)
congestion[i, j, h] = round(gamma, 4)
uncertainty[i, j, h] = round(eta, 4)
congestion_penalty[i, j, h] = round(rho, 4)
return TrafficData(
n_nodes=n_nodes,
n_intervals=n_intervals,
travel_time=travel_time,
congestion=congestion,
uncertainty=uncertainty,
congestion_penalty=congestion_penalty,
)
# ------------------------------------------------------------------
# Main Generation Pipeline
# ------------------------------------------------------------------
def generate_all(self, output_dir: Optional[Path] = None) -> dict:
"""Run the complete data generation pipeline.
Returns a dict with all generated data structures.
"""
if output_dir is None:
output_dir = Path(__file__).parent.parent / "data" / "synthetic"
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
# 1. Depot
depot = Depot()
# 2. Customers
customers = self.generate_customers()
# 3. Arcs
arcs = self.generate_arcs(depot, customers)
# 4. Traffic tensors
traffic = self.generate_traffic(arcs)
# 5. Save to disk
self._save_data(output_dir, depot, customers, arcs, traffic)
return {
"depot": depot,
"customers": customers,
"arcs": arcs,
"traffic": traffic,
"n_nodes": 1 + self.n_customers,
"n_customers": self.n_customers,
"n_vehicles": self.n_vehicles,
"vehicle_capacity": self.vehicle_capacity,
}
def _save_data(self, output_dir: Path, depot: Depot,
customers: List[Customer], arcs: List[Arc],
traffic: TrafficData):
"""Save generated data to CSV and numpy files."""
# Customers CSV
cust_df = pd.DataFrame([{
"customer_id": c.customer_id,
"x_km": c.x_km,
"y_km": c.y_km,
"demand_kg": c.demand_kg,
"service_time_min": c.service_time_min,
"earliest_time_min": c.earliest_time_min,
"latest_time_min": c.latest_time_min,
"cluster": c.cluster,
} for c in customers])
cust_df.to_csv(output_dir / "customers.csv", index=False)
# Depot CSV
depot_df = pd.DataFrame([{
"depot_id": depot.depot_id,
"x_km": depot.x_km,
"y_km": depot.y_km,
}])
depot_df.to_csv(output_dir / "depot.csv", index=False)
# Vehicles CSV
veh_df = pd.DataFrame([{
"vehicle_id": k + 1,
"capacity_kg": self.vehicle_capacity,
"max_operating_min": self.op_end - self.op_start,
} for k in range(self.n_vehicles)])
veh_df.to_csv(output_dir / "vehicles.csv", index=False)
# Arcs CSV
arc_records = []
for arc in arcs:
arc_records.append({
"from_node": arc.from_node,
"to_node": arc.to_node,
"distance_km": arc.distance_km,
"road_type": arc.road_type,
"speed_kmh": arc.speed_kmh,
"base_travel_time_min": arc.base_travel_time_min,
})
pd.DataFrame(arc_records).to_csv(output_dir / "arcs.csv", index=False)
# Traffic tensors as numpy
np.save(output_dir / "travel_time.npy", traffic.travel_time)
np.save(output_dir / "congestion.npy", traffic.congestion)
np.save(output_dir / "uncertainty.npy", traffic.uncertainty)
np.save(output_dir / "congestion_penalty.npy", traffic.congestion_penalty)
# Metadata
meta = {
"n_customers": self.n_customers,
"n_vehicles": self.n_vehicles,
"n_nodes": 1 + self.n_customers,
"n_intervals": self.n_intervals,
"n_arcs": len(arcs),
"area_width_km": self.area_w,
"area_height_km": self.area_h,
"operating_start_min": self.op_start,
"operating_end_min": self.op_end,
"vehicle_capacity_kg": self.vehicle_capacity,
"seed": self.seed,
"congestion_multipliers": self.traffic_multipliers.tolist(),
}
with open(output_dir / "metadata.yaml", "w") as f:
yaml.dump(meta, f)
print(f"Dataset generated: {output_dir}")
print(f" Customers: {len(customers)}")
print(f" Arcs: {len(arcs)}")
print(f" Traffic tensor: {traffic.shape}")
print(f" Total data points: ~{traffic.travel_time.size * 3}")
if __name__ == "__main__":
gen = DataGenerator(seed=42)
data = gen.generate_all()
print("\nGeneration complete.")
for c in data["customers"][:5]:
print(f" Customer {c.customer_id}: ({c.x_km}, {c.y_km}) "
f"demand={c.demand_kg}kg window=[{c.earliest_time_min}, {c.latest_time_min}]")

View File

@@ -0,0 +1,204 @@
"""
Main comparison experiment (v2).
Runs all 5 algorithms with configurable settings.
Supports --config flag for version switching.
Outputs results to version-specific directories.
Includes statistical significance testing.
"""
import sys
import os
import time
import argparse
import numpy as np
import pandas as pd
import yaml
from pathlib import Path
from tqdm import tqdm
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from src.data_generator import DataGenerator
from src.problem import ProblemContext
from src.cost import CostCalculator
from src.baselines.static_vrptw import StaticVRPTWSolver
from src.baselines.ta_greedy import TAGreedySolver
from src.alns.alns_base import ALNSBase
from src.tabu.t_alns import TALNS
from src.rrd.t_alns_rrd import TALNSRRD
try:
from scipy import stats as scipy_stats
HAS_SCIPY = True
except ImportError:
HAS_SCIPY = False
def run_experiment(
config_name="calibrated",
n_seeds=None,
max_iterations=None,
time_limit_sec=None,
output_dir=None,
quick=False,
):
config_path = Path(__file__).parent.parent.parent / "configs" / f"{config_name}.yaml"
with open(config_path) as f:
cfg = yaml.safe_load(f)
if output_dir is None:
output_dir = Path(__file__).parent.parent.parent / "results" / config_name
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
(output_dir / "tables").mkdir(exist_ok=True)
(output_dir / "figures").mkdir(exist_ok=True)
(output_dir / "logs").mkdir(exist_ok=True)
import shutil
shutil.copy(config_path, output_dir / "config_used.yaml")
if quick:
n_seeds = n_seeds or 3
max_iterations = max_iterations or 100
time_limit_sec = time_limit_sec or 60
else:
n_seeds = n_seeds or cfg.get("experiments", {}).get("random_seeds", 10)
max_iterations = max_iterations or cfg.get("alns", {}).get("max_iterations", 500)
time_limit_sec = time_limit_sec or cfg.get("alns", {}).get("time_limit_sec", 300)
print("=" * 70)
print(f"T-ALNS-RRD Main Comparison [{config_name}]")
print(f"Seeds: {n_seeds}, Iter: {max_iterations}, Time: {time_limit_sec}s")
print("=" * 70)
print("\n[1/5] Generating dataset...")
gen = DataGenerator(config_path=str(config_path), seed=42)
data = gen.generate_all()
ctx = ProblemContext(
customers={c.customer_id: c for c in data["customers"]},
depot=data["depot"], traffic=data["traffic"],
n_vehicles=data["n_vehicles"], vehicle_capacity=data["vehicle_capacity"],
)
cost_calc = CostCalculator(
lambda_lateness=cfg["cost"]["lambda_lateness"],
lambda_congestion=cfg["cost"]["lambda_congestion"],
lambda_stability=cfg["cost"]["lambda_stability"],
)
rrd_cfg = cfg.get("rrd", {})
alg_cfg = {
"max_iterations": max_iterations,
"time_limit_sec": time_limit_sec,
"event_probability": rrd_cfg.get("events", {}).get("event_probability", 0.3),
"event_check_interval": rrd_cfg.get("events", {}).get("event_check_interval", 10),
"reward_global_best": 1.0, "reward_improvement": 0.5,
"reward_accepted": 0.2, "reward_rejected": 0.0,
}
algorithms = [
("Static-VRPTW", StaticVRPTWSolver, {}),
("TA-VRPTW-Greedy", TAGreedySolver, {}),
("ALNS-Base", ALNSBase, alg_cfg),
("T-ALNS", TALNS, alg_cfg),
("T-ALNS-RRD", TALNSRRD, alg_cfg),
]
results = []
all_metrics = {}
all_convergence = {}
for alg_name, solver_cls, solver_cfg in algorithms:
print(f"\n[{alg_name}] Running {n_seeds} seeds...")
alg_results = []
seed_costs = []
for seed in tqdm(range(n_seeds)):
if alg_name in ("Static-VRPTW", "TA-VRPTW-Greedy"):
solver = solver_cls(ctx, cost_calc)
else:
solver = solver_cls(ctx, cost_calc, config=solver_cfg)
r = solver.run(seed=seed)
r["seed"] = seed; r["algorithm"] = alg_name
alg_results.append(r)
seed_costs.append(r["total_cost"])
if "convergence_history" in r and r["convergence_history"]:
all_convergence[f"{alg_name}_s{seed}"] = r["convergence_history"]
all_metrics[alg_name] = seed_costs
df = pd.DataFrame(alg_results)
stats = {
"algorithm": alg_name,
"total_cost_mean": df["total_cost"].mean(),
"total_cost_std": df["total_cost"].std(),
"otdr_mean": df["otdr"].mean() * 100,
"otdr_std": df["otdr"].std() * 100,
"ces_mean": df["ces"].mean(),
"ces_std": df["ces"].std(),
"travel_time_mean": df["travel_time_cost"].mean(),
"delay_penalty_mean": df["delay_penalty"].mean(),
"congestion_cost_mean": df["congestion_cost"].mean(),
"computation_time_mean": df["computation_time"].mean(),
"avg_delay_mean": df["avg_delay"].mean(),
"max_delay_mean": df["max_delay"].mean(),
"late_customers_mean": df["late_customers"].mean(),
}
results.append(stats)
print(f" Cost={stats['total_cost_mean']:.1f}±{stats['total_cost_std']:.1f} OTDR={stats['otdr_mean']:.1f}% CES={stats['ces_mean']:.1f}")
results_df = pd.DataFrame(results)
results_df.to_csv(output_dir / "tables" / "main_comparison.csv", index=False)
raw_df = pd.DataFrame({k: v for k, v in all_metrics.items()})
raw_df.to_csv(output_dir / "tables" / "per_seed_costs.csv", index=False)
if all_convergence:
np.savez(output_dir / "logs" / "convergence.npz", **all_convergence)
if HAS_SCIPY and n_seeds >= 5 and cfg.get("experiments", {}).get("statistical_testing", True):
print("\n" + "=" * 70)
print("STATISTICAL ANALYSIS (paired t-tests)")
print("=" * 70)
algo_names = list(all_metrics.keys())
stat_results = []
for i in range(len(algo_names)):
for j in range(i + 1, len(algo_names)):
a1, a2 = algo_names[i], algo_names[j]
t_stat, p_val = scipy_stats.ttest_rel(all_metrics[a1], all_metrics[a2])
sig = "***" if p_val < 0.001 else "**" if p_val < 0.01 else "*" if p_val < 0.05 else "ns"
print(f" {a1:<18} vs {a2:<18} t={t_stat:6.2f} p={p_val:.4f} {sig}")
stat_results.append({"algo_a": a1, "algo_b": a2, "t_statistic": t_stat, "p_value": p_val, "significant": sig})
pd.DataFrame(stat_results).to_csv(output_dir / "tables" / "statistical_tests.csv", index=False)
print("\n" + "=" * 70)
print(f"FINAL TABLE [{config_name}]")
print("=" * 70)
print(f"{'Algorithm':<20} | {'Total Cost':>14} | {'OTDR':>9} | {'CES':>10} | {'Time':>8}")
print("-" * 75)
baseline_cost = results[0]["total_cost_mean"]
for r in results:
imp = (baseline_cost - r["total_cost_mean"]) / baseline_cost * 100 if baseline_cost > 0 else 0
print(f"{r['algorithm']:<20} | {r['total_cost_mean']:>8.1f}±{r['total_cost_std']:>4.1f} | "
f"{r['otdr_mean']:>5.1f}{r['otdr_std']:>3.1f}% | "
f"{r['ces_mean']:>7.1f}±{r['ces_std']:>3.1f} | "
f"{r['computation_time_mean']:>6.1f}s")
print(f"\nSaved to {output_dir}")
return results_df
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--config", default="calibrated")
parser.add_argument("--seeds", type=int, default=None)
parser.add_argument("--iterations", type=int, default=None)
parser.add_argument("--time-limit", type=int, default=None)
parser.add_argument("--output", default=None)
parser.add_argument("--quick", action="store_true")
args = parser.parse_args()
run_experiment(
config_name=args.config, n_seeds=args.seeds,
max_iterations=args.iterations, time_limit_sec=args.time_limit,
output_dir=args.output, quick=args.quick,
)

View File

@@ -0,0 +1,183 @@
"""
Tabu Memory Convergence & Ablation Experiment.
Runs ALNS-Base vs T-ALNS variants with per-iteration cost tracking
to demonstrate how Tabu memory prevents search stagnation.
Configurations:
1. ALNS-Base (no memory)
2. T-ALNS Move Tabu only
3. T-ALNS Frequency Memory only
4. Full T-ALNS (all 3 components)
Tracks best_cost at EVERY iteration for convergence analysis.
"""
import sys, os, time, argparse
import numpy as np
import pandas as pd
import yaml
from pathlib import Path
from tqdm import tqdm
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from src.data_generator import DataGenerator
from src.problem import ProblemContext
from src.cost import CostCalculator
from src.alns.alns_base import ALNSBase
from src.tabu.t_alns import TALNS
def run_tabu_experiment(
config_name="calibrated",
n_seeds=5,
max_iterations=1000,
time_limit_sec=600,
output_dir=None,
):
config_path = Path(__file__).parent.parent.parent / "configs" / f"{config_name}.yaml"
with open(config_path) as f:
cfg = yaml.safe_load(f)
if output_dir is None:
output_dir = Path(__file__).parent.parent.parent / "results" / f"{config_name}_tabu"
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
(output_dir / "tables").mkdir(exist_ok=True)
(output_dir / "figures").mkdir(exist_ok=True)
(output_dir / "logs").mkdir(exist_ok=True)
import shutil
shutil.copy(config_path, output_dir / "config_used.yaml")
print("=" * 70)
print(f"TABU MEMORY EXPERIMENT [{config_name}]")
print(f"Seeds: {n_seeds}, Iter: {max_iterations}, Time: {time_limit_sec}s")
print("=" * 70)
print("\n[1/3] Generating dataset...")
gen = DataGenerator(config_path=str(config_path), seed=42)
data = gen.generate_all()
ctx = ProblemContext(
customers={c.customer_id: c for c in data["customers"]},
depot=data["depot"], traffic=data["traffic"],
n_vehicles=data["n_vehicles"], vehicle_capacity=data["vehicle_capacity"],
)
cc = CostCalculator(
lambda_lateness=cfg["cost"]["lambda_lateness"],
lambda_congestion=cfg["cost"]["lambda_congestion"],
)
alg_config = {
"max_iterations": max_iterations,
"time_limit_sec": time_limit_sec,
"reward_global_best": 1.0, "reward_improvement": 0.5,
"reward_accepted": 0.2, "reward_rejected": 0.0,
}
# Configurations to test
configs = [
("ALNS-Base (no Tabu)", ALNSBase, {}),
("+ Move Tabu only", TALNS, {"disable_solution_tabu": True, "disable_frequency_memory": True}),
("+ Frequency Memory only", TALNS, {"disable_move_tabu": True, "disable_solution_tabu": True}),
("Full T-ALNS", TALNS, {}),
]
all_results = []
all_convergence = {}
for config_name, solver_cls, extra_cfg in configs:
full_cfg = {**alg_config, **extra_cfg}
print(f"\n[{config_name}] Running {n_seeds} seeds...")
seed_results = []
seed_convs = []
for seed in tqdm(range(n_seeds)):
solver = solver_cls(ctx, cc, config=full_cfg)
r = solver.run(seed=seed)
r["seed"] = seed
r["configuration"] = config_name
seed_results.append(r)
# Record per-iteration best cost
if hasattr(solver, 'best_cost_history') and solver.best_cost_history:
seed_convs.append(list(solver.best_cost_history))
df = pd.DataFrame(seed_results)
stats = {
"configuration": config_name,
"total_cost_mean": df["total_cost"].mean(),
"total_cost_std": df["total_cost"].std(),
"otdr_mean": df["otdr"].mean() * 100,
"otdr_std": df["otdr"].std() * 100,
"ces_mean": df["ces"].mean(),
"ces_std": df["ces"].std(),
"computation_time_mean": df["computation_time"].mean(),
"iterations_mean": df["iterations"].mean() if "iterations" in df.columns else max_iterations,
}
all_results.append(stats)
if seed_convs:
# Average convergence across seeds, padded to same length
max_len = max(len(c) for c in seed_convs)
padded = []
for c in seed_convs:
if len(c) >= max_len:
padded.append(c[:max_len])
else:
padded.append(c + [c[-1]] * (max_len - len(c)))
avg_conv = np.mean(padded, axis=0)
all_convergence[config_name] = avg_conv.tolist()
print(f" Cost: {stats['total_cost_mean']:.1f} ± {stats['total_cost_std']:.1f}")
print(f" OTDR: {stats['otdr_mean']:.1f}% ± {stats['otdr_std']:.1f}%")
print(f" Time: {stats['computation_time_mean']:.1f}s")
# Save results
results_df = pd.DataFrame(all_results)
results_df.to_csv(output_dir / "tables" / "tabu_ablation.csv", index=False)
# Save per-seed data
seed_dfs = []
for i, (cname, _, _) in enumerate(configs):
pass
if all_convergence:
conv_df = pd.DataFrame(all_convergence)
conv_df.to_csv(output_dir / "tables" / "tabu_convergence.csv", index=False)
np.savez(output_dir / "logs" / "tabu_convergence.npz", **all_convergence)
# Print summary
print("\n" + "=" * 70)
print("TABU ABLATION RESULTS")
print("=" * 70)
print(f"{'Configuration':<30} | {'Total Cost':>14} | {'OTDR':>8} | {'CES':>10}")
print("-" * 70)
baseline = all_results[0]["total_cost_mean"]
for r in all_results:
imp = (baseline - r["total_cost_mean"]) / baseline * 100
print(f"{r['configuration']:<30} | {r['total_cost_mean']:>8.1f}±{r['total_cost_std']:>4.1f} | "
f"{r['otdr_mean']:>5.1f}{r['otdr_std']:>2.1f}% | {r['ces_mean']:>7.1f}±{r['ces_std']:>3.1f}")
print(f"\nSaved to {output_dir}")
return all_results, all_convergence
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--config", default="calibrated")
parser.add_argument("--seeds", type=int, default=5)
parser.add_argument("--iterations", type=int, default=1000)
parser.add_argument("--time-limit", type=int, default=600)
parser.add_argument("--output", default=None)
args = parser.parse_args()
run_tabu_experiment(
config_name=args.config,
n_seeds=args.seeds,
max_iterations=args.iterations,
time_limit_sec=args.time_limit,
output_dir=args.output,
)

View File

@@ -0,0 +1,175 @@
"""
Problem definition for the DVRPTW-TA (Dynamic Vehicle Routing Problem
with Time Windows and Traffic Awareness).
Defines Route, Solution, and ProblemContext classes aligned with
the paper's formulation (§3.1).
"""
import copy
import hashlib
from typing import Dict, List, Optional, Tuple
import numpy as np
class Route:
"""A single vehicle's delivery route: sequence of customer nodes.
Format: [0, c1, c2, ..., ck, 0] where 0 is the depot.
"""
def __init__(self, vehicle_id: int, nodes: Optional[List[int]] = None):
self.vehicle_id = vehicle_id
self.nodes: List[int] = nodes if nodes is not None else [0, 0]
@property
def customers(self) -> List[int]:
"""Return customer nodes only (excluding depot(s))."""
return [n for n in self.nodes if n != 0]
@property
def n_customers(self) -> int:
return len(self.customers)
def total_demand(self, customers: Dict[int, "Customer"]) -> float:
"""Sum of demand for all customers on this route."""
return sum(customers[n].demand_kg for n in self.customers if n in customers)
def is_capacity_feasible(self, capacity: float, customers: Dict[int, "Customer"]) -> bool:
return self.total_demand(customers) <= capacity
def insert(self, customer_id: int, position: int):
"""Insert a customer at the given position (after depot start)."""
# position is 1-based index in the customer sequence
assert 1 <= position <= len(self.customers) + 1
# Find insertion point in self.nodes (skip leading depot)
insert_at = position
self.nodes.insert(insert_at, customer_id)
def remove(self, customer_id: int) -> bool:
"""Remove a customer from the route. Returns True if found."""
if customer_id in self.nodes:
self.nodes.remove(customer_id)
return True
return False
def copy(self) -> "Route":
return Route(self.vehicle_id, list(self.nodes))
def __repr__(self) -> str:
return f"Route(v{self.vehicle_id}: {self.nodes})"
class Solution:
"""A complete routing solution with m routes (one per vehicle)."""
def __init__(self, n_vehicles: int):
self.n_vehicles = n_vehicles
self.routes: List[Route] = [Route(k) for k in range(n_vehicles)]
def copy(self) -> "Solution":
s = Solution(self.n_vehicles)
s.routes = [r.copy() for r in self.routes]
return s
def find_route(self, customer_id: int) -> Optional[int]:
"""Find which vehicle (route index) serves a customer."""
for k, route in enumerate(self.routes):
if customer_id in route.nodes:
return k
return None
def find_position(self, customer_id: int) -> Optional[Tuple[int, int]]:
"""Find (vehicle_idx, position_in_nodes) for a customer."""
for k, route in enumerate(self.routes):
try:
pos = route.nodes.index(customer_id)
return (k, pos)
except ValueError:
continue
return None
def unassigned_customers(self, all_customer_ids: set) -> set:
"""Return set of customer IDs not assigned to any route."""
assigned = set()
for route in self.routes:
assigned.update(route.customers)
return all_customer_ids - assigned
def get_assignments(self) -> Dict[int, int]:
"""Return {customer_id: vehicle_id} for all assigned customers."""
result = {}
for k, route in enumerate(self.routes):
for c in route.customers:
result[c] = k
return result
def total_customers(self) -> int:
return sum(len(r.customers) for r in self.routes)
def __repr__(self) -> str:
parts = [f"Solution({self.n_vehicles} vehicles, {self.total_customers()} customers)"]
for r in self.routes:
parts.append(f" {r}")
return "\n".join(parts)
class ProblemContext:
"""Bundles all problem instance data for efficient access."""
def __init__(
self,
customers: Dict[int, "Customer"],
depot: "Depot",
traffic: "TrafficData",
n_vehicles: int,
vehicle_capacity: float,
op_start: float = 360.0,
op_end: float = 1080.0,
n_intervals: int = 12,
):
self.customers = customers
self.depot = depot
self.traffic = traffic
self.n_vehicles = n_vehicles
self.vehicle_capacity = vehicle_capacity
self.op_start = op_start
self.op_end = op_end
self.n_intervals = n_intervals
self.n_nodes = 1 + len(customers)
@property
def customer_ids(self) -> set:
return set(self.customers.keys())
@property
def interval_duration(self) -> float:
return (self.op_end - self.op_start) / self.n_intervals
def time_to_interval(self, minutes: float) -> int:
"""Map time in minutes to interval index h ∈ [0, n_intervals-1]."""
minutes = max(self.op_start, min(minutes, self.op_end - 1))
return int((minutes - self.op_start) / self.interval_duration)
def get_travel_time(self, i: int, j: int, depart_minutes: float) -> float:
"""Get time-dependent travel time for arc (i,j) at departure time."""
if i == j:
return 0.0
h = self.time_to_interval(depart_minutes)
val = self.traffic.travel_time[i, j, h]
return val if not np.isinf(val) else np.inf
def get_congestion_penalty(self, i: int, j: int, depart_minutes: float) -> float:
"""Get congestion penalty ρ_ij(T_i) from the traffic tensor."""
if i == j:
return 0.0
h = self.time_to_interval(depart_minutes)
return self.traffic.congestion_penalty[i, j, h]
def get_risk_adjusted_time(self, i: int, j: int, depart_minutes: float,
beta: float = 0.3) -> float:
"""Get risk-adjusted travel time: t'_ij = t_ij + β·η_ij (Eq.10)."""
t = self.get_travel_time(i, j, depart_minutes)
h = self.time_to_interval(depart_minutes)
eta = self.traffic.uncertainty[i, j, h]
return t + beta * eta

View File

@@ -0,0 +1,232 @@
"""
Candidate action generation for RRD (paper §3.3.3).
Generates a set of candidate actions for each event type:
E1: Local reroute (k-shortest detours), customer reassignment
E2: Insert into existing routes, delayed insertion, subcontract
E3: Load redistribution (transfer stops to other vehicles)
E4: Local resequencing (2-opt), temporary tolerance
Max actions |A_e| ≤ 20 per event.
"""
import numpy as np
from typing import List, Tuple, Optional
from ..problem import Solution, Route
from ..cost import CostCalculator
def generate_actions_E1_traffic(
event: "Event",
solution: Solution,
problem_ctx: "ProblemContext",
cost_calc: CostCalculator,
rng: np.random.Generator,
) -> List[dict]:
"""Generate actions for traffic incident (E1).
- Local reroute: enumerate up to K_s=5 FIFO-consistent detours
- Customer reassignment: move affected customer to nearby vehicle
"""
actions = []
arc = event.affected_arc
affected_cust = event.affected_customer
# Action 1: Local reroute - find alternative paths
if arc[0] >= 0 and arc[1] >= 0:
# Simplified: try bypass routes through different intermediate nodes
# In a complete graph, we can try different node orderings
for bypass_node in range(1, problem_ctx.n_nodes):
if bypass_node == arc[0] or bypass_node == arc[1]:
continue
# Bypass: go through bypass_node instead of direct arc
actions.append({
"type": "local_reroute",
"arc": arc,
"bypass": bypass_node,
"description": f"Reroute via node {bypass_node}",
})
if len(actions) >= 5:
break
# Action 2: Customer reassignment - try other vehicles
if affected_cust > 0:
current_route_idx = solution.find_route(affected_cust)
for k in range(solution.n_vehicles):
if k == current_route_idx:
continue
route = solution.routes[k]
if route.total_demand(problem_ctx.customers) + problem_ctx.customers[affected_cust].demand_kg <= problem_ctx.vehicle_capacity:
actions.append({
"type": "customer_reassign",
"customer": affected_cust,
"target_vehicle": k,
"description": f"Reassign customer {affected_cust} to vehicle {k}",
})
if len(actions) >= 10:
break
return actions[:20]
def generate_actions_E2_urgent(
event: "Event",
solution: Solution,
problem_ctx: "ProblemContext",
cost_calc: CostCalculator,
rng: np.random.Generator,
) -> List[dict]:
"""Generate actions for urgent delivery (E2).
- Immediate insertion into each vehicle
- Delayed insertion (up to 30 min shift)
"""
actions = []
extra = getattr(event, "_extra", {})
new_x = extra.get("x_km", problem_ctx.depot.x_km)
new_y = extra.get("y_km", problem_ctx.depot.y_km)
new_demand = extra.get("demand_kg", 5.0)
deadline = extra.get("deadline", 720.0)
for k, route in enumerate(solution.routes):
if route.total_demand(problem_ctx.customers) + new_demand > problem_ctx.vehicle_capacity:
continue
# Immediate insertion (find best position)
# We approximate: try inserting at each position
n_cust_pos = len(route.customers)
for pos in range(1, n_cust_pos + 2):
actions.append({
"type": "urgent_insert",
"target_vehicle": k,
"position": pos,
"demand_kg": new_demand,
"deadline": deadline,
"delayed": False,
"description": f"Insert urgent delivery into vehicle {k} pos {pos}",
})
break # Just try one position per vehicle for speed
if len(actions) >= 10:
break
# Subcontract option (penalty-based)
actions.append({
"type": "subcontract",
"penalty_cost": 200.0,
"description": "Subcontract delivery (fixed penalty)",
})
return actions[:20]
def generate_actions_E3_capacity(
event: "Event",
solution: Solution,
problem_ctx: "ProblemContext",
cost_calc: CostCalculator,
rng: np.random.Generator,
) -> List[dict]:
"""Generate actions for capacity violation (E3).
- Redistribute stops to nearest vehicle with capacity
"""
actions = []
affected = event.affected_customer
if affected <= 0:
return actions
current_route = solution.find_route(affected)
if current_route is None:
return actions
route = solution.routes[current_route]
customers_on_route = route.customers
# Try moving the affected customer to other vehicles
for k in range(solution.n_vehicles):
if k == current_route:
continue
target_route = solution.routes[k]
cust = problem_ctx.customers[affected]
if target_route.total_demand(problem_ctx.customers) + cust.demand_kg <= problem_ctx.vehicle_capacity:
actions.append({
"type": "redistribute",
"customer": affected,
"source_vehicle": current_route,
"target_vehicle": k,
"description": f"Move customer {affected} from vehicle {current_route} to {k}",
})
return actions[:20]
def generate_actions_E4_timewindow(
event: "Event",
solution: Solution,
problem_ctx: "ProblemContext",
cost_calc: CostCalculator,
rng: np.random.Generator,
) -> List[dict]:
"""Generate actions for time window risk (E4).
- Local resequencing (2-opt swap)
- Temporary time window tolerance
"""
actions = []
affected = event.affected_customer
route_idx = solution.find_route(affected)
if route_idx is None:
return actions
route = solution.routes[route_idx]
customers = route.customers
if len(customers) < 2:
return actions
# Action 1-3: 2-opt swaps within the route
for i in range(len(customers)):
for j in range(i + 2, len(customers)):
actions.append({
"type": "resequence_2opt",
"vehicle": route_idx,
"swap_i": customers[i],
"swap_j": customers[j],
"description": f"2-opt swap {customers[i]} <-> {customers[j]}",
})
if len(actions) >= 5:
break
if len(actions) >= 5:
break
# Action: Temporary tolerance (accept delay with penalty)
actions.append({
"type": "temporary_tolerance",
"customer": affected,
"tolerance_min": 30.0,
"description": f"Grant 30min tolerance for customer {affected}",
})
return actions[:20]
def generate_candidate_actions(
event: "Event",
solution: Solution,
problem_ctx: "ProblemContext",
cost_calc: CostCalculator,
rng: np.random.Generator,
) -> List[dict]:
"""Dispatch to the appropriate action generator based on event type."""
event_type = event.event_type
if event_type.value == "traffic_incident":
return generate_actions_E1_traffic(event, solution, problem_ctx, cost_calc, rng)
elif event_type.value == "urgent_delivery":
return generate_actions_E2_urgent(event, solution, problem_ctx, cost_calc, rng)
elif event_type.value == "capacity_violation":
return generate_actions_E3_capacity(event, solution, problem_ctx, cost_calc, rng)
elif event_type.value == "time_window_risk":
return generate_actions_E4_timewindow(event, solution, problem_ctx, cost_calc, rng)
else:
return []

View File

@@ -0,0 +1,223 @@
"""
Dispatch decision module (paper §3.3.3, Eq.40-42).
Selects the best action via composite scoring:
Σ(a,e,s) = ω₁ × V^adjusted + ω₂ × Stability(a,s) + ω₃ × Recovery(a,s)
Stability (Eq.41): minimizes route structure change
Recovery (Eq.42): proximity to optimal T-ALNS solution
"""
import time
import numpy as np
from typing import List, Dict, Optional
from ..problem import Solution, ProblemContext
from ..cost import CostCalculator
from .candidate_actions import generate_candidate_actions
from .rollout import RolloutEngine
class Dispatch:
"""Real-time dispatch decision maker."""
def __init__(
self,
problem_ctx: ProblemContext,
cost_calc: CostCalculator,
rollout_engine: RolloutEngine,
weight_rollout: float = 0.4,
weight_stability: float = 0.3,
weight_recovery: float = 0.3,
seed: int = None,
):
self.ctx = problem_ctx
self.cost_calc = cost_calc
self.rollout_engine = rollout_engine
self.w_rollout = weight_rollout # ω₁
self.w_stability = weight_stability # ω₂
self.w_recovery = weight_recovery # ω₃
self.rng = np.random.default_rng(seed)
self.dispatch_log: List[dict] = []
def compute_stability(self, action: dict, current_solution: Solution) -> float:
"""Compute route stability score (Eq.41).
Stability = Σ_k |R^a_k ∩ R^s_k| / |R^a_k R^s_k|
Higher = more stable (less disruption). Range [0, 1].
"""
# Simulate applying the action
simulated = self.rollout_engine._apply_action(current_solution, action)
total_stability = 0.0
for k in range(current_solution.n_vehicles):
new_set = set(simulated.routes[k].customers)
old_set = set(current_solution.routes[k].customers)
union = new_set | old_set
if len(union) == 0:
total_stability += 1.0
else:
inter = new_set & old_set
total_stability += len(inter) / len(union)
return total_stability / current_solution.n_vehicles
def compute_recovery(self, action: dict, optimal_solution: Solution) -> float:
"""Compute recovery score (Eq.42).
Recovery = -Σ_k Σ_i |OptPosition(i) - CurrentPosition(i,a)|
Lower displacement = better recovery. We invert for scoring.
"""
if optimal_solution is None:
return 0.5 # Neutral
simulated = self.rollout_engine._apply_action(optimal_solution, action)
total_displacement = 0.0
n_assigned = 0
# Compare customer positions
opt_positions = {}
for k, route in enumerate(optimal_solution.routes):
for pos, c in enumerate(route.customers):
opt_positions[c] = (k, pos)
for k, route in enumerate(simulated.routes):
for pos, c in enumerate(route.customers):
if c in opt_positions:
opt_k, opt_pos = opt_positions[c]
# Penalize vehicle change more than position change
if k != opt_k:
total_displacement += 5.0
total_displacement += abs(pos - opt_pos)
n_assigned += 1
avg_displacement = total_displacement / max(n_assigned, 1)
# Invert: lower displacement = higher recovery score
recovery_score = 1.0 / (1.0 + avg_displacement)
return recovery_score
def compute_composite_score(
self,
action: dict,
rollout_value: float,
stability_score: float,
recovery_score: float,
) -> float:
"""Compute composite dispatch score (Eq.40).
Σ = ω₁ × V^adjusted + ω₂ × Stability + ω₃ × Recovery
Note: We normalize so higher score = better action.
We invert rollout_value since lower cost is better.
"""
# Invert rollout value (lower cost = higher score)
v_inv = 1.0 / max(rollout_value, 1.0)
score = (
self.w_rollout * v_inv
+ self.w_stability * stability_score
+ self.w_recovery * recovery_score
)
return score
def select_action(
self,
event: "Event",
solution: Solution,
optimal_solution: Solution = None,
tabu_structures: dict = None,
) -> Optional[dict]:
"""Select the best dispatch action for an event.
Returns the selected action dict or None if no action is beneficial.
"""
t_start = time.time()
# Generate candidate actions
actions = generate_candidate_actions(
event, solution, self.ctx, self.cost_calc, self.rng
)
if not actions:
return None
# Evaluate each action
scored_actions = []
for action in actions:
# Rollout evaluation
V = self.rollout_engine.evaluate_action(
solution, action, event, tabu_structures
)
# Stability score
stability = self.compute_stability(action, solution)
# Recovery score
recovery = self.compute_recovery(action, optimal_solution)
# Composite score
score = self.compute_composite_score(action, V, stability, recovery)
scored_actions.append({
"action": action,
"rollout_value": V,
"stability": stability,
"recovery": recovery,
"composite_score": score,
})
# Select best
scored_actions.sort(key=lambda x: x["composite_score"], reverse=True)
best = scored_actions[0]
elapsed_ms = (time.time() - t_start) * 1000.0
# Log the decision
log_entry = {
"event_type": event.event_type.value,
"time": event.time_min,
"urgency": event.urgency_score,
"selected_action": best["action"]["type"],
"rollout_value": best["rollout_value"],
"composite_score": best["composite_score"],
"response_time_ms": elapsed_ms,
"n_actions_evaluated": len(actions),
}
self.dispatch_log.append(log_entry)
return best["action"]
def apply_action(
self,
action: dict,
solution: Solution,
) -> Solution:
"""Apply the selected action to the current solution."""
return self.rollout_engine._apply_action(solution, action)
def get_statistics(self) -> dict:
"""Get dispatch statistics for reporting."""
if not self.dispatch_log:
return {
"total_events": 0,
"success_rate": 0.0,
"avg_response_ms": 0.0,
"total_cost_reduction": 0.0,
}
n_events = len(self.dispatch_log)
# Success rate: all dispatched events count as "handled"
success_rate = 1.0
avg_response = np.mean([e["response_time_ms"] for e in self.dispatch_log])
return {
"total_events": n_events,
"success_rate": success_rate,
"avg_response_ms": avg_response,
"dispatch_log": self.dispatch_log,
}

View File

@@ -0,0 +1,316 @@
"""
Real-time event detection and classification (paper §3.3.3, Eq.35).
Detects four event types:
E1: Critical traffic incidents (road closures, severe congestion)
E2: Urgent delivery insertions (new high-priority orders)
E3: Vehicle capacity violations (demand fluctuations)
E4: Service time violations (cumulative delays risking time windows)
Computes urgency scores and triggers dispatch when thresholds exceeded.
"""
import numpy as np
from typing import List, Dict, Optional, Tuple
from dataclasses import dataclass
from enum import Enum
class EventType(Enum):
E1_TRAFFIC = "traffic_incident"
E2_URGENT = "urgent_delivery"
E3_CAPACITY = "capacity_violation"
E4_TIMEWINDOW = "time_window_risk"
@dataclass
class Event:
"""A detected disruption event requiring real-time dispatch."""
event_type: EventType
time_min: float # When event is detected
affected_customer: int # Which customer is affected (-1 if none)
affected_arc: Tuple[int, int] # Which arc is affected (-1,-1 if none)
urgency_score: float # Ψ(e,t) from Eq.35
description: str
class EventGenerator:
"""Generates and detects disruption events during simulation."""
def __init__(
self,
problem_ctx: "ProblemContext",
cost_calc: "CostCalculator",
urgency_threshold: float = 0.5,
event_weights: dict = None,
):
self.ctx = problem_ctx
self.cost_calc = cost_calc
self.urgency_threshold = urgency_threshold
# Event-specific weights for urgency calculation (Eq.35)
# α_e: deadline pressure, β_e: impact severity, γ_e: cost increase
self.event_weights = event_weights or {
EventType.E1_TRAFFIC: (0.5, 0.3, 0.2),
EventType.E2_URGENT: (0.7, 0.1, 0.2),
EventType.E3_CAPACITY: (0.3, 0.4, 0.3),
EventType.E4_TIMEWINDOW: (0.8, 0.1, 0.1),
}
self.rng = np.random.default_rng(None)
self.event_log: List[Event] = []
def generate_traffic_incident(
self,
current_time: float,
solution: "Solution",
) -> Optional[Event]:
"""Generate an E1 traffic incident on a random arc.
Simulates sudden congestion on a segment of a vehicle's route.
"""
# Pick a random route with customers
active_routes = [r for r in solution.routes if len(r.nodes) > 2]
if not active_routes:
return None
route = self.rng.choice(active_routes)
nodes = route.nodes
# Pick a random arc that isn't depot-depot
valid_arcs = [(nodes[i], nodes[i+1]) for i in range(len(nodes)-1)
if not (nodes[i] == 0 and nodes[i+1] == 0)]
if not valid_arcs:
return None
arc = valid_arcs[self.rng.integers(len(valid_arcs))]
affected_customer = arc[1] if arc[1] != 0 else arc[0]
# Compute urgency
alpha, beta, gamma = self.event_weights[EventType.E1_TRAFFIC]
t_horizon = 120.0 # 2 hours horizon
# Deadline pressure: how soon does the affected customer need service?
if affected_customer in self.ctx.customers:
cust = self.ctx.customers[affected_customer]
deadline = cust.latest_time_min
time_factor = max(0, (deadline - current_time) / t_horizon)
time_factor = 1.0 - min(time_factor, 1.0) # Invert: tighter deadline = higher urgency
else:
time_factor = 0.5
# Impact: severity of congestion (multiplier 2-3x)
impact = self.rng.uniform(0.5, 1.0)
# Cost increase: estimated cost delta
cost_increase = self.rng.uniform(0.2, 0.8)
urgency = alpha * time_factor + beta * impact + gamma * cost_increase
event = Event(
event_type=EventType.E1_TRAFFIC,
time_min=current_time,
affected_customer=affected_customer,
affected_arc=arc,
urgency_score=urgency,
description=f"Traffic incident on arc {arc} affecting customer {affected_customer}",
)
# Actually modify travel time tensor to simulate congestion
# Use moderate severity and auto-restore after dispatch
i, j = arc
if i >= 0 and j >= 0:
severity = 1.5 + self.rng.uniform(0, 0.5) # 1.5x-2.0x (moderate)
event._travel_backup_ij = self.ctx.traffic.travel_time[i, j, :].copy()
event._congestion_backup_ij = self.ctx.traffic.congestion_penalty[i, j, :].copy()
event._modified_arc = (i, j)
self.ctx.traffic.travel_time[i, j, :] *= severity
self.ctx.traffic.congestion_penalty[i, j, :] *= severity
return event
def generate_urgent_delivery(
self,
current_time: float,
solution: "Solution",
) -> Optional[Event]:
"""Generate an E2 urgent delivery insertion.
A new high-priority customer appears with a tight time window.
"""
# Create a synthetic urgent customer near the depot
depot_x = self.ctx.depot.x_km
depot_y = self.ctx.depot.y_km
new_x = depot_x + self.rng.uniform(-2, 2)
new_y = depot_y + self.rng.uniform(-2, 2)
new_demand = self.rng.uniform(3, 8)
deadline = current_time + self.rng.uniform(30, 90)
# Generate a temporary customer ID (negative to avoid collision)
temp_id = -1 # For event tracking only
alpha, beta, gamma = self.event_weights[EventType.E2_URGENT]
t_horizon = 120.0
time_factor = max(0, (deadline - current_time) / t_horizon)
time_factor = 1.0 - min(time_factor, 1.0)
impact = 0.3 # Single customer impact
cost_increase = self.rng.uniform(0.3, 0.7)
urgency = alpha * time_factor + beta * impact + gamma * cost_increase
event = Event(
event_type=EventType.E2_URGENT,
time_min=current_time,
affected_customer=temp_id,
affected_arc=(-1, -1),
urgency_score=urgency,
description=f"Urgent delivery near ({new_x:.1f},{new_y:.1f}) demand={new_demand}kg deadline={deadline:.0f}min",
)
# Store extra data for the dispatch handler
event._extra = {
"x_km": new_x,
"y_km": new_y,
"demand_kg": new_demand,
"deadline": deadline,
}
return event
def generate_capacity_violation(
self,
current_time: float,
solution: "Solution",
) -> Optional[Event]:
"""Generate an E3 capacity violation.
A customer's demand increases mid-route, exceeding vehicle capacity.
"""
active_routes = [r for r in solution.routes if r.customers]
if not active_routes:
return None
route = self.rng.choice(active_routes)
if not route.customers:
return None
affected = self.rng.choice(route.customers)
demand_increase = self.rng.uniform(5, 20)
alpha, beta, gamma = self.event_weights[EventType.E3_CAPACITY]
time_factor = 0.5 # Capacity issues are less time-dependent
impact = min(1.0, demand_increase / self.ctx.vehicle_capacity)
cost_increase = self.rng.uniform(0.3, 0.7)
urgency = alpha * time_factor + beta * impact + gamma * cost_increase
event = Event(
event_type=EventType.E3_CAPACITY,
time_min=current_time,
affected_customer=affected,
affected_arc=(-1, -1),
urgency_score=urgency,
description=f"Capacity violation: customer {affected} demand increased by {demand_increase:.1f}kg",
)
event._extra = {"demand_increase": demand_increase}
return event
def generate_time_window_risk(
self,
current_time: float,
solution: "Solution",
) -> Optional[Event]:
"""Generate an E4 service time violation risk.
A customer is predicted to be served late given current progress.
"""
# Find routes with customers
active_routes = [r for r in solution.routes if r.customers]
if not active_routes:
return None
# Evaluate route timing and find customers at risk
at_risk = []
for route in active_routes:
result = self.cost_calc.propagate_route(route.nodes, self.ctx)
for idx, node in enumerate(route.nodes):
if node == 0:
continue
delay = result["delays"][idx]
service_start = result["service_starts"][idx]
if node in self.ctx.customers:
cust = self.ctx.customers[node]
# At risk if service start is close to or past deadline
slack = cust.latest_time_min - service_start
if slack < 30 and slack > -60: # Within 30 min of deadline
at_risk.append((node, slack, route))
if not at_risk:
return None
# Pick the most at-risk customer
at_risk.sort(key=lambda x: x[1]) # Sort by slack (lowest first)
affected, slack, route = at_risk[0]
alpha, beta, gamma = self.event_weights[EventType.E4_TIMEWINDOW]
t_horizon = 60.0
time_factor = 1.0 - max(0, (slack + 60) / t_horizon)
time_factor = min(1.0, max(0, time_factor))
impact = self.rng.uniform(0.3, 0.8)
cost_increase = max(0, -slack / 30.0) # penalty proportional to lateness
urgency = alpha * time_factor + beta * impact + gamma * cost_increase
event = Event(
event_type=EventType.E4_TIMEWINDOW,
time_min=current_time,
affected_customer=affected,
affected_arc=(-1, -1),
urgency_score=urgency,
description=f"Time window risk: customer {affected} has {slack:.0f}min slack",
)
return event
def detect_events(
self,
current_time: float,
solution: "Solution",
event_probability: float = 0.3,
) -> List[Event]:
"""Detect events at the current time step.
Returns list of events with urgency > threshold.
Each event type has an independent probability of occurring.
"""
events = []
# E1: Traffic incident (30% chance)
if self.rng.random() < event_probability:
ev = self.generate_traffic_incident(current_time, solution)
if ev and ev.urgency_score > self.urgency_threshold:
events.append(ev)
# E2: Urgent delivery (15% chance)
if self.rng.random() < event_probability * 0.5:
ev = self.generate_urgent_delivery(current_time, solution)
if ev and ev.urgency_score > self.urgency_threshold:
events.append(ev)
# E3: Capacity violation (10% chance)
if self.rng.random() < event_probability * 0.3:
ev = self.generate_capacity_violation(current_time, solution)
if ev and ev.urgency_score > self.urgency_threshold:
events.append(ev)
# E4: Time window risk (40% chance)
if self.rng.random() < event_probability * 1.3:
ev = self.generate_time_window_risk(current_time, solution)
if ev and ev.urgency_score > self.urgency_threshold:
events.append(ev)
self.event_log.extend(events)
return events
def reset(self):
"""Reset event log for a new run."""
self.event_log = []
self.rng = np.random.default_rng(None)

View File

@@ -0,0 +1,255 @@
"""
Rollout simulation engine (paper §3.3.3, Eq.36-39).
Performs bounded-horizon Monte Carlo simulations to evaluate
candidate dispatch actions under traffic uncertainty.
Rollout horizon H: 30-120 minutes
Monte Carlo iterations: 2-50
"""
import time
import numpy as np
from typing import List, Dict, Optional
from ..problem import Solution, ProblemContext
from ..cost import CostCalculator
class RolloutEngine:
"""Rollout-based evaluation of dispatch actions.
Simulates the future state under each candidate action using
simplified traffic predictions and returns adjusted cost estimates.
"""
def __init__(
self,
problem_ctx: ProblemContext,
cost_calc: CostCalculator,
horizon_min: int = 30,
horizon_max: int = 120,
n_sim_min: int = 2,
n_sim_max: int = 50,
mc_iterations: int = 50,
tabu_penalty: float = 50.0,
tabu_bonus: float = 25.0,
seed: int = None,
):
self.ctx = problem_ctx
self.cost_calc = cost_calc
self.horizon_min = horizon_min
self.horizon_max = horizon_max
self.n_sim_min = n_sim_min
self.n_sim_max = n_sim_max
self.mc_iterations = mc_iterations
self.tabu_penalty = tabu_penalty
self.tabu_bonus = tabu_bonus
self.rng = np.random.default_rng(seed)
def adapt_horizon(self, urgency: float) -> int:
"""Adapt rollout horizon based on event urgency (Eq.43).
H = max(H_min, H_max - α × Ψ(e,t))
"""
alpha = 1.0
h = int(self.horizon_max - alpha * urgency * self.horizon_max)
return max(self.horizon_min, min(h, self.horizon_max))
def adapt_sim_count(self, available_time_ms: float) -> int:
"""Adapt simulation count based on available time (Eq.44).
N_sim = max(N_min, ⌊(T_available - T_overhead) / T_sim⌋)
"""
overhead = 10.0 # ms
per_sim = 50.0 # ms
n = int((available_time_ms - overhead) / per_sim)
return max(self.n_sim_min, min(n, self.n_sim_max))
def extrapolate_travel_time(
self, i: int, j: int, depart_time: float, delta_min: float
) -> float:
"""Piecewise-linear travel time extrapolation (Eq.38).
t_ij(T_i + s) = t_ij^(r) + s/Δt × (t_ij^(r+1) - t_ij^(r))
"""
h = self.ctx.time_to_interval(depart_time)
t_current = self.ctx.get_travel_time(i, j, depart_time)
# Get next interval's travel time
depart_next = depart_time + delta_min
h_next = self.ctx.time_to_interval(depart_next)
if h_next != h:
t_next = self.ctx.traffic.travel_time[i, j, h_next]
else:
t_next = t_current
# Linear interpolation
interval_dur = self.ctx.interval_duration
frac = delta_min / interval_dur
t_extrap = t_current + frac * (t_next - t_current)
return max(t_current * 0.5, t_extrap)
def simulate_horizon(
self,
solution: Solution,
action: dict,
horizon_min: int,
noise_std: float = 0.05,
) -> float:
"""Simulate one rollout over the horizon.
Returns the total cost over the simulated horizon.
"""
import copy
sim_sol = solution.copy()
sim_sol = self._apply_action(sim_sol, action)
total_cost = 0.0
current_time = self.ctx.op_start
# Simple forward simulation: evaluate each route with noise
for route in sim_sol.routes:
if len(route.nodes) < 2:
continue
nodes = route.nodes
for idx in range(1, len(nodes)):
i, j = nodes[idx - 1], nodes[idx]
# Skip negative/invalid node IDs
if i < 0 or i >= self.ctx.n_nodes or j < 0 or j >= self.ctx.n_nodes:
continue
# Add noise to travel time
base_tt = self.ctx.get_travel_time(i, j, current_time)
noise = 1.0 + self.rng.normal(0, noise_std)
tt = max(base_tt * noise, 0.0)
current_time += tt
# Congestion cost
congestion = self.ctx.get_congestion_penalty(i, j, current_time)
total_cost += tt + self.cost_calc.lambda_congestion * congestion
# Delay penalty
if j != 0 and j in self.ctx.customers:
cust = self.ctx.customers[j]
lateness = max(0.0, current_time - cust.latest_time_min)
total_cost += self.cost_calc.lambda_lateness * lateness
# Service time
if j != 0 and j in self.ctx.customers:
current_time += self.ctx.customers[j].service_time_min
return total_cost
def evaluate_action(
self,
solution: Solution,
action: dict,
event: "Event",
tabu_structures: dict = None,
) -> float:
"""Evaluate a single action via Monte Carlo rollouts (Eq.36-39).
Returns the adjusted rollout value.
"""
urgency = event.urgency_score
horizon = self.adapt_horizon(urgency)
# Monte Carlo simulations
costs = []
n_sims = min(self.n_sim_min + 5, self.mc_iterations)
for _ in range(n_sims):
cost = self.simulate_horizon(
solution, action, horizon,
noise_std=0.05 + 0.1 * urgency,
)
costs.append(cost)
V_rollout = np.mean(costs)
# Tabu adjustments (Eq.39)
V_adjusted = V_rollout
if tabu_structures is not None:
tabu_mem = tabu_structures.get("move_tabu")
if tabu_mem is not None:
# Check if action resembles a tabu move
customer_val = action.get("customer", [])
if isinstance(customer_val, list):
removed = set(customer_val)
elif isinstance(customer_val, int) and customer_val > 0:
removed = {customer_val}
else:
removed = set()
if removed:
is_tabu = tabu_mem.is_tabu(removed, action["type"], "", 0)
if is_tabu:
V_adjusted -= self.tabu_penalty
else:
V_adjusted += self.tabu_bonus
return V_adjusted
def _apply_action(self, solution: Solution, action: dict) -> Solution:
"""Apply a candidate action to a solution (returns modified copy)."""
import copy
sol = solution.copy()
action_type = action.get("type", "")
if action_type == "local_reroute":
# Simplified: we don't actually modify the route structure
# In practice, this would insert a bypass node
pass
elif action_type == "customer_reassign":
cust = action.get("customer")
target = action.get("target_vehicle")
if cust and target is not None:
# Remove from current route
current = sol.find_route(cust)
if current is not None:
sol.routes[current].remove(cust)
# Add to target route (append at end)
if target < sol.n_vehicles:
insert_pos = len(sol.routes[target].customers) + 1
sol.routes[target].insert(cust, insert_pos)
elif action_type == "urgent_insert":
target = action.get("target_vehicle")
pos = action.get("position", 1)
# Create temp customer ID (negative)
temp_id = -100 # placeholder
if target is not None and target < sol.n_vehicles:
sol.routes[target].insert(temp_id, pos)
elif action_type == "subcontract":
# No route change, just penalty
pass
elif action_type == "redistribute":
cust = action.get("customer")
target = action.get("target_vehicle")
if cust and target is not None:
current = sol.find_route(cust)
if current is not None:
sol.routes[current].remove(cust)
if target < sol.n_vehicles:
insert_pos = len(sol.routes[target].customers) + 1
sol.routes[target].insert(cust, insert_pos)
elif action_type == "resequence_2opt":
vehicle_idx = action.get("vehicle")
swap_i = action.get("swap_i")
swap_j = action.get("swap_j")
if vehicle_idx is not None and swap_i and swap_j:
route = sol.routes[vehicle_idx]
try:
idx_i = route.nodes.index(swap_i)
idx_j = route.nodes.index(swap_j)
route.nodes[idx_i], route.nodes[idx_j] = route.nodes[idx_j], route.nodes[idx_i]
except ValueError:
pass
elif action_type == "temporary_tolerance":
# Accept delay without route change
pass
return sol

View File

@@ -0,0 +1,301 @@
"""
T-ALNS-RRD: Tabu-guided ALNS with Rollout-based Real-Time Dispatch (Algorithm 3).
Extends T-ALNS with a real-time dispatch layer for handling disruptions.
Maintains two logical threads:
1. Main T-ALNS optimization (background)
2. Event monitoring and dispatch (interleaved)
When events are detected, dispatch actions are evaluated via rollout
simulations and the best action is applied immediately.
"""
import time
import numpy as np
from typing import Dict, List, Optional
from ..problem import Solution, ProblemContext
from ..cost import CostCalculator
from ..tabu.t_alns import TALNS
from .event_generator import EventGenerator
from .rollout import RolloutEngine
from .dispatch import Dispatch
class TALNSRRD:
"""Tabu-guided ALNS with Rollout-based Real-Time Dispatch.
Implements Algorithm 3 from the paper.
"""
def __init__(
self,
problem_ctx: ProblemContext,
cost_calc: CostCalculator,
config: dict = None,
):
self.ctx = problem_ctx
self.cost_calc = cost_calc
# Default config (merges ALNS + Tabu + RRD configs)
default_cfg = {
# ALNS
"max_iterations": 1000,
"time_limit_sec": 600,
"destroy_ratio_min": 0.1,
"destroy_ratio_max": 0.4,
"initial_temperature_factor": 0.05,
"cooling_rate": 0.99975,
"reaction_factor": 0.1,
"segment_length": 100,
"stall_limit": 200,
"max_attempts": 5,
"reward_global_best": 1.0,
"reward_improvement": 0.5,
"reward_accepted": 0.2,
"reward_rejected": 0.0,
# RRD
"rollout_horizon_min": 30,
"rollout_horizon_max": 120,
"rollout_n_sim_min": 2,
"rollout_n_sim_max": 50,
"dispatch_weight_rollout": 0.4,
"dispatch_weight_stability": 0.3,
"dispatch_weight_recovery": 0.3,
"event_probability": 0.3,
"event_check_interval": 10, # Check for events every N iterations
}
if config:
default_cfg.update(config)
self.cfg = default_cfg
# Initialize T-ALNS core
self.talns = TALNS(problem_ctx, cost_calc, config)
# Initialize RRD components
self.event_generator = EventGenerator(
problem_ctx, cost_calc,
urgency_threshold=0.3, # Lower threshold for more events
)
self.rollout_engine = RolloutEngine(
problem_ctx, cost_calc,
horizon_min=self.cfg["rollout_horizon_min"],
horizon_max=self.cfg["rollout_horizon_max"],
n_sim_min=self.cfg["rollout_n_sim_min"],
n_sim_max=self.cfg["rollout_n_sim_max"],
)
self.dispatch = Dispatch(
problem_ctx, cost_calc, self.rollout_engine,
weight_rollout=self.cfg["dispatch_weight_rollout"],
weight_stability=self.cfg["dispatch_weight_stability"],
weight_recovery=self.cfg["dispatch_weight_recovery"],
)
# Stats
self.iteration_history: List[float] = []
self.best_cost_history: List[float] = []
self.event_count = 0
self.dispatched_count = 0
def solve(self, seed: int = None) -> Solution:
"""Run T-ALNS-RRD optimization (Algorithm 3)."""
rng = np.random.default_rng(seed)
t_start = time.time()
# Reset components
self.talns.move_tabu.clear()
self.talns.sol_tabu.clear()
self.talns.freq_mem.clear()
self.event_generator.reset()
# Initialize weights
self.talns.destroy_weights = {name: 1.0 for name in self.talns.destroy_ops}
self.talns.repair_weights = {name: 1.0 for name in self.talns.repair_ops}
# Build initial solution
S_current = self.talns._construct_initial(seed=seed)
S_best = S_current.copy()
current_cost = self.cost_calc.compute_total_cost(S_current, self.ctx)
best_cost = current_cost
# Initialize SA
from ..alns.acceptance import SimulatedAnnealing
initial_temp = self.cfg["initial_temperature_factor"] * max(current_cost, 1.0)
sa = SimulatedAnnealing(
initial_temp=initial_temp,
cooling_rate=self.cfg["cooling_rate"],
seed=seed,
)
n_customers = len(self.ctx.customers)
q_min = max(1, int(self.cfg["destroy_ratio_min"] * n_customers))
q_max = max(q_min + 1, int(self.cfg["destroy_ratio_max"] * n_customers))
stall_counter = 0
last_best_iter = 0
iter_count = 0
self.iteration_history = []
self.best_cost_history = []
# Main loop (Algorithm 3 - single thread simulation)
while iter_count < self.cfg["max_iterations"]:
elapsed = time.time() - t_start
if elapsed > self.cfg["time_limit_sec"]:
break
if stall_counter >= self.cfg["stall_limit"]:
break
# --- Event monitoring (interleaved) ---
if iter_count % self.cfg["event_check_interval"] == 0:
sim_time = self.ctx.op_start + (iter_count / self.cfg["max_iterations"]) * (self.ctx.op_end - self.ctx.op_start)
events = self.event_generator.detect_events(
sim_time, S_current, self.cfg["event_probability"]
)
for event in events:
self.event_count += 1
# Get tabu structures for dispatch
tabu_structs = {
"move_tabu": self.talns.move_tabu,
}
# Select and apply dispatch action
action = self.dispatch.select_action(
event, S_current, S_best, tabu_structs
)
if action is not None:
S_current = self.dispatch.apply_action(action, S_current)
self.dispatched_count += 1
# Restore traffic tensor after dispatch (event handled)
modified_arc = getattr(event, '_modified_arc', None)
if modified_arc is not None:
i, j = modified_arc
backup = getattr(event, '_travel_backup_ij', None)
if backup is not None:
self.ctx.traffic.travel_time[i, j, :] = backup
cb = getattr(event, '_congestion_backup_ij', None)
if cb is not None:
self.ctx.traffic.congestion_penalty[i, j, :] = cb
# Update cost after dispatch
current_cost = self.cost_calc.compute_total_cost(S_current, self.ctx)
# Update Tabu memory with dispatch action
self.talns.sol_tabu.add(S_current, iter_count)
self.talns.freq_mem.update(S_current)
if current_cost < best_cost:
S_best = S_current.copy()
best_cost = current_cost
last_best_iter = iter_count
# --- Standard T-ALNS iteration ---
# Compute diversification
delta = self.talns._compute_diversification_intensity(iter_count, last_best_iter)
if delta > self.cfg.get("diversification_delta_max", 0.7):
self.talns._modified_d = self.talns._modify_probabilities(delta, self.talns.destroy_weights)
self.talns._modified_r = self.talns._modify_probabilities(delta, self.talns.repair_weights)
d_weights = self.talns._modified_d
r_weights = self.talns._modified_r
else:
d_weights = self.talns.destroy_weights
r_weights = self.talns.repair_weights
accepted = False
for attempt in range(self.cfg["max_attempts"]):
d_name = self.talns._select_operator(d_weights, rng)
r_name = self.talns._select_operator(r_weights, rng)
q = rng.integers(q_min, q_max + 1)
# Destroy
S_temp = S_current.copy()
if d_name == "worst":
S_temp, removed = self.talns.destroy_ops[d_name](
S_temp, self.ctx, self.cost_calc, rng, q
)
elif d_name == "related":
S_temp, removed = self.talns.destroy_ops[d_name](
S_temp, self.ctx, rng, q
)
else:
S_temp, removed = self.talns.destroy_ops[d_name](S_temp, rng, q)
if not removed:
continue
# Check move tabu
if self.talns.move_tabu.is_tabu(set(removed), d_name, r_name, iter_count):
continue
# Repair
S_new = self.talns.repair_ops[r_name](
S_temp, removed, self.ctx, self.cost_calc, rng
)
# Check solution tabu
if self.talns.sol_tabu.is_tabu(S_new, iter_count):
new_cost_temp = self.cost_calc.compute_total_cost(S_new, self.ctx)
if not self.talns._check_aspiration(S_new, new_cost_temp, best_cost, removed):
continue
new_cost = self.cost_calc.compute_total_cost(S_new, self.ctx)
# Acceptance
if sa.accept(current_cost, new_cost):
S_current = S_new
current_cost = new_cost
self.talns.move_tabu.add(set(removed), d_name, r_name, iter_count)
self.talns.sol_tabu.add(S_new, iter_count)
self.talns.freq_mem.update(S_new)
if new_cost < best_cost:
S_best = S_new.copy()
best_cost = new_cost
stall_counter = 0
last_best_iter = iter_count
self.talns.move_tabu.update_tenure(found_improvement=True)
reward = self.cfg["reward_global_best"]
else:
stall_counter += 1
self.talns.move_tabu.update_tenure(found_improvement=False)
reward = self.cfg["reward_improvement"]
accepted = True
else:
reward = self.cfg["reward_rejected"]
if iter_count % self.cfg["segment_length"] == 0:
self.talns._update_weights(d_name, r_name, reward)
break
if not accepted:
stall_counter += 1
sa.cool()
self.iteration_history.append(current_cost)
self.best_cost_history.append(best_cost)
iter_count += 1
self._iter_count = iter_count
self._best_cost = best_cost
return S_best
def run(self, seed: int = None) -> dict:
"""Run solver and return comprehensive metrics."""
t0 = time.time()
solution = self.solve(seed=seed)
elapsed = time.time() - t0
metrics = self.cost_calc.evaluate_solution(solution, self.ctx)
metrics["computation_time"] = elapsed
metrics["algorithm"] = "T-ALNS-RRD"
metrics["iterations"] = getattr(self, "_iter_count", 0)
metrics["convergence_history"] = self.best_cost_history
metrics["events_detected"] = self.event_count
metrics["events_dispatched"] = self.dispatched_count
metrics["dispatch_log"] = self.dispatch.dispatch_log
return metrics

View File

@@ -0,0 +1,121 @@
"""
Attribute-based Frequency Memory (paper Eq.25-26, Eq.33).
Tracks customer-vehicle assignments and temporal positions
to guide diversification toward underrepresented configurations.
"""
import numpy as np
from typing import Dict
class FrequencyMemory:
"""Attribute-based frequency memory for search diversification.
Tracks:
- F^cv: customer-vehicle assignment frequency matrix (Eq.25)
- F^tp: temporal position frequency matrix (Eq.26)
"""
def __init__(
self,
n_customers: int,
n_vehicles: int,
normalization_factor: float = 2.0,
normalization_interval: int = 50,
):
self.n_customers = n_customers
self.n_vehicles = n_vehicles
self.normalization_factor = normalization_factor # κ
self.normalization_interval = normalization_interval # ν
# Customer-vehicle frequency: F^cv[i, k]
# i = customer index (1..n_customers), k = vehicle index (0..n_vehicles-1)
self.cv_freq = np.zeros((n_customers + 1, n_vehicles), dtype=np.float64)
# Temporal position frequency: F^tp[i, j]
# How often customer i appears at position j in any route
self.max_positions = n_customers + 1
self.tp_freq = np.zeros(
(n_customers + 1, self.max_positions), dtype=np.float64
)
self._iter_since_norm = 0
def update(
self,
solution: "Solution",
congestion_weights=None, # Optional congestion-aware weighting (Eq.34)
):
"""Update frequency matrices from a solution.
Args:
solution: Current solution to record.
congestion_weights: Optional dict of {(i,j): ρ} for weighted update.
"""
# Update customer-vehicle assignment frequency
for k, route in enumerate(solution.routes):
for c in route.customers:
weight = 1.0
if congestion_weights is not None:
# Eq.34: weight by congestion on incoming arcs
for idx in range(1, len(route.nodes)):
if route.nodes[idx] == c:
prev = route.nodes[idx - 1]
weight = 1.0 + congestion_weights.get((prev, c), 0.0)
break
self.cv_freq[c, k] += weight
# Update temporal position frequency
for k, route in enumerate(solution.routes):
customers = route.customers
for pos_idx, c in enumerate(customers):
if pos_idx < self.max_positions:
self.tp_freq[c, pos_idx] += 1
self._iter_since_norm += 1
if self._iter_since_norm >= self.normalization_interval:
self._normalize()
self._iter_since_norm = 0
def _normalize(self):
"""Normalize frequency matrices to prevent overflow (Eq.32)."""
self.cv_freq = np.floor(self.cv_freq / self.normalization_factor)
self.tp_freq = np.floor(self.tp_freq / self.normalization_factor)
def get_assignment_frequency(self, customer_id: int, vehicle_id: int) -> float:
"""Get how often customer has been assigned to vehicle."""
return self.cv_freq[customer_id, vehicle_id]
def get_position_frequency(self, customer_id: int, position: int) -> float:
"""Get how often customer appears at a given position."""
if position >= self.max_positions:
return 0.0
return self.tp_freq[customer_id, position]
def get_least_used_vehicle(self, customer_id: int) -> int:
"""Get the vehicle least frequently assigned to a customer."""
freqs = self.cv_freq[customer_id, :]
return int(np.argmin(freqs))
def get_mean_assignment_freq(self) -> float:
"""Get mean customer-vehicle assignment frequency."""
return float(self.cv_freq.mean())
def get_std_assignment_freq(self) -> float:
"""Get standard deviation of assignment frequencies (σ in Eq.27)."""
return float(self.cv_freq.std())
def is_low_frequency(
self, customer_id: int, vehicle_id: int, beta: float = 0.3
) -> bool:
"""Check if assignment is low-frequency (for aspiration, Eq.30).
True if F^cv_ik < β × mean(F^cv).
"""
mean_freq = self.get_mean_assignment_freq()
return self.cv_freq[customer_id, vehicle_id] < beta * mean_freq
def clear(self):
self.cv_freq.fill(0.0)
self.tp_freq.fill(0.0)
self._iter_since_norm = 0

View File

@@ -0,0 +1,96 @@
"""
Move-based Tabu list (paper Eq.22-23, Eq.32).
Records recent destroy-repair operations to prevent cycling.
Each entry: (removed_set, destroy_op, repair_op, iteration)
"""
from typing import List, Set, Tuple, Optional
from collections import deque
class MoveTabu:
"""Move-based Tabu memory.
Tracks recent (C_removed, h_d, h_r) tuples to avoid revisiting
similar destroy-repair operations.
"""
def __init__(
self,
tenure: int = 7,
tenure_min: int = 3,
tenure_max: int = 12,
overlap_threshold: float = 0.5,
stall_for_increase: int = 50,
):
self.tenure = tenure
self.tenure_min = tenure_min
self.tenure_max = tenure_max
self.overlap_threshold = overlap_threshold # μ in Eq.23
self.stall_for_increase = stall_for_increase
self._entries: deque = deque() # (removed_set, d_op, r_op, iteration)
self._stall_counter = 0
self._last_improvement_iter = 0
def is_tabu(
self,
removed_customers: Set[int],
destroy_op: str,
repair_op: str,
current_iter: int,
) -> bool:
"""Check if a move is Tabu (Eq.23).
Tabu if exists entry (Ĉ, ĥ_d, ĥ_r, ť) such that:
|C_removed ∩ Ĉ| ≥ μ × min(|C_removed|, |Ĉ|)
AND current_iter - ť ≤ τ_move
"""
if not removed_customers:
return False
for entry in self._entries:
c_hat, _, _, t_hat = entry
overlap = len(removed_customers & c_hat)
threshold = self.overlap_threshold * min(
len(removed_customers), len(c_hat)
)
if overlap >= threshold and (current_iter - t_hat) <= self.tenure:
return True
return False
def add(
self,
removed_customers: Set[int],
destroy_op: str,
repair_op: str,
iteration: int,
):
"""Record a move in the Tabu list."""
self._entries.append(
(frozenset(removed_customers), destroy_op, repair_op, iteration)
)
# Evict old entries (FIFO)
while len(self._entries) > self.tenure_max:
self._entries.popleft()
def update_tenure(self, found_improvement: bool):
"""Adapt tenure based on improvement (Eq.32).
τ_move(t+1) = min(τ_move + 1, τ_max) if no improvement
= max(τ_move - 1, τ_min) if improvement found
"""
if found_improvement:
self.tenure = max(self.tenure - 1, self.tenure_min)
self._stall_counter = 0
else:
self._stall_counter += 1
if self._stall_counter >= self.stall_for_increase:
self.tenure = min(self.tenure + 1, self.tenure_max)
self._stall_counter = 0
def clear(self):
self._entries.clear()
self.tenure = 7
self._stall_counter = 0

View File

@@ -0,0 +1,73 @@
"""
Solution-based Tabu memory (paper Eq.24).
Stores hash-based encodings of routing structures to avoid
revisiting previously explored solutions.
"""
from typing import Set
from collections import deque
class SolutionTabu:
"""Solution-based Tabu memory.
Uses a polynomial hash function to encode routing structures.
Avoids revisiting solutions recorded in recent iterations.
"""
def __init__(
self,
tenure: int = 15,
buffer_size: int = 1000,
hash_prime: int = 1000000007,
):
self.tenure = tenure
self.buffer_size = buffer_size
self.hash_prime = hash_prime
self._hashes: deque = deque() # (hash_value, iteration)
@staticmethod
def compute_hash(solution: "Solution", prime: int = 1000000007) -> int:
"""Compute locality-sensitive hash of a solution (Eq.24).
H(S) = Σ_k Σ_i φ(v_ki, v_k(i+1)) mod P
Uses polynomial hash on consecutive customer pairs.
"""
h = 0
base = 31
for route in solution.routes:
nodes = route.nodes
for idx in range(len(nodes) - 1):
a, b = nodes[idx], nodes[idx + 1]
# Customer pairs get a unique contribution
pair_val = a * 1000 + b
h = (h * base + pair_val) % prime
return h
def is_tabu(self, solution: "Solution", current_iter: int) -> bool:
"""Check if a solution's hash appears in Tabu memory."""
h = self.compute_hash(solution, self.hash_prime)
for stored_hash, stored_iter in self._hashes:
if stored_hash == h and (current_iter - stored_iter) <= self.tenure:
return True
return False
def add(self, solution: "Solution", iteration: int):
"""Record solution hash in Tabu memory."""
h = self.compute_hash(solution, self.hash_prime)
self._hashes.append((h, iteration))
# Evict old entries
while len(self._hashes) > self.buffer_size:
self._hashes.popleft()
def contains_hash(self, hash_val: int, current_iter: int) -> bool:
"""Check if a hash value is Tabu."""
for stored_hash, stored_iter in self._hashes:
if stored_hash == hash_val and (current_iter - stored_iter) <= self.tenure:
return True
return False
def clear(self):
self._hashes.clear()

View File

@@ -0,0 +1,411 @@
"""
T-ALNS: Tabu-enhanced Adaptive Large Neighborhood Search (Algorithm 2).
Extends ALNS-Base with multi-layered Tabu memory:
- Move-based Tabu (prevent revisiting similar operations)
- Solution-based Tabu (prevent revisiting same routing structures)
- Frequency memory (guide diversification toward underrepresented configs)
Also includes aspiration criteria (Eq.29-31) and diversification
intensity control (Eq.27-28).
"""
import time
import numpy as np
from typing import Dict, List, Optional
from ..problem import Solution, ProblemContext
from ..cost import CostCalculator
from ..alns.operators_destroy import destroy_random, destroy_worst, destroy_related
from ..alns.operators_repair import repair_greedy, repair_regret2, repair_time_window_aware
from ..alns.acceptance import SimulatedAnnealing
from .move_tabu import MoveTabu
from .solution_tabu import SolutionTabu
from .frequency_memory import FrequencyMemory
class TALNS:
"""Tabu-enhanced Adaptive Large Neighborhood Search.
Implements Algorithm 2 from the paper.
"""
def __init__(
self,
problem_ctx: ProblemContext,
cost_calc: CostCalculator,
config: dict = None,
):
self.ctx = problem_ctx
self.cost_calc = cost_calc
# Default config
default_cfg = {
"max_iterations": 1000,
"time_limit_sec": 600,
"destroy_ratio_min": 0.1,
"destroy_ratio_max": 0.4,
"initial_temperature_factor": 0.05,
"cooling_rate": 0.99975,
"reaction_factor": 0.1,
"segment_length": 100,
"stall_limit": 200,
"max_attempts": 5,
"reward_global_best": 1.0,
"reward_improvement": 0.5,
"reward_accepted": 0.2,
"reward_rejected": 0.0,
# Tabu config
"move_tabu_tenure": 7,
"move_tabu_tenure_min": 3,
"move_tabu_tenure_max": 12,
"move_tabu_overlap_threshold": 0.5,
"move_tabu_stall_for_increase": 50,
"solution_tabu_tenure": 15,
"solution_tabu_buffer": 1000,
"solution_tabu_prime": 1000000007,
"freq_norm_factor": 2.0,
"freq_norm_interval": 50,
"diversification_delta_max": 0.7,
"diversification_eta": 0.5,
"diversification_weights": [0.4, 0.3, 0.3],
"aspiration_beta": 0.3,
"aspiration_gamma": 0.8,
# Component toggles for ablation
"disable_move_tabu": False,
"disable_solution_tabu": False,
"disable_frequency_memory": False,
}
if config:
default_cfg.update(config)
self.cfg = default_cfg
# Operators
self.destroy_ops = {
"random": destroy_random,
"worst": destroy_worst,
"related": destroy_related,
}
self.repair_ops = {
"greedy": repair_greedy,
"regret2": repair_regret2,
"time_window": repair_time_window_aware,
}
# Tabu structures
self.move_tabu = MoveTabu(
tenure=self.cfg["move_tabu_tenure"],
tenure_min=self.cfg["move_tabu_tenure_min"],
tenure_max=self.cfg["move_tabu_tenure_max"],
overlap_threshold=self.cfg["move_tabu_overlap_threshold"],
stall_for_increase=self.cfg["move_tabu_stall_for_increase"],
)
self.sol_tabu = SolutionTabu(
tenure=self.cfg["solution_tabu_tenure"],
buffer_size=self.cfg["solution_tabu_buffer"],
hash_prime=self.cfg["solution_tabu_prime"],
)
self.freq_mem = FrequencyMemory(
n_customers=len(self.ctx.customers),
n_vehicles=self.ctx.n_vehicles,
normalization_factor=self.cfg["freq_norm_factor"],
normalization_interval=self.cfg["freq_norm_interval"],
)
# Weights
self.destroy_weights: Dict[str, float] = {}
self.repair_weights: Dict[str, float] = {}
# Stats
self.iteration_history: List[float] = []
self.best_cost_history: List[float] = []
def _construct_initial(self, seed: int = None) -> Solution:
"""Build initial solution using greedy insertion (same as ALNS)."""
rng = np.random.default_rng(seed)
solution = Solution(self.ctx.n_vehicles)
customer_ids = list(self.ctx.customers.keys())
rng.shuffle(customer_ids)
for cid in customer_ids:
cust = self.ctx.customers[cid]
best_route = -1
best_pos = -1
best_cost = float("inf")
for k, route in enumerate(solution.routes):
if route.total_demand(self.ctx.customers) + cust.demand_kg > self.ctx.vehicle_capacity:
continue
pos, cost = self.cost_calc.find_best_insertion(route, cid, self.ctx, use_full=True)
if cost < best_cost:
best_cost = cost
best_route = k
best_pos = pos
if best_route >= 0:
solution.routes[best_route].insert(cid, best_pos)
return solution
def _compute_diversification_intensity(
self, current_iter: int, last_best_iter: int
) -> float:
"""Compute diversification intensity δ(t) (Eq.27).
δ(t) = ω₁×(t-t_last_best)/T_max + ω₂×|T_move|/|T_move|_max + ω₃×σ(F^cv)
"""
w1, w2, w3 = self.cfg["diversification_weights"]
t_max = self.cfg["max_iterations"]
# Time since last improvement (normalized)
time_factor = (current_iter - last_best_iter) / max(t_max, 1)
# Move Tabu utilization
move_factor = self.move_tabu.tenure / self.cfg["move_tabu_tenure_max"]
# Frequency std
freq_std = self.freq_mem.get_std_assignment_freq()
freq_factor = min(freq_std / max(freq_std, 1.0), 1.0)
delta = w1 * time_factor + w2 * move_factor + w3 * freq_factor
return delta
def _modify_probabilities(
self,
delta_current: float,
base_probs: Dict[str, float],
) -> Dict[str, float]:
"""Modify operator selection probabilities for diversification (Eq.28).
p'_h = η × p_h + (1 - η) × div_h / Σ div_j
Higher frequency memory → more diversification potential.
"""
eta = self.cfg["diversification_eta"]
total_base = sum(base_probs.values())
base_norm = {k: v / max(total_base, 1e-10) for k, v in base_probs.items()}
# Assign diversification potential: inverse of usage frequency
div_scores = {}
for name in base_probs:
# Rough diversification: less-used operators get higher score
div_scores[name] = 1.0 / max(base_probs.get(name, 0.1), 0.01)
total_div = sum(div_scores.values())
div_norm = {k: v / max(total_div, 1e-10) for k, v in div_scores.items()}
modified = {}
for name in base_probs:
modified[name] = eta * base_norm.get(name, 0.0) + (1 - eta) * div_norm.get(name, 0.0)
return modified
def _select_operator(self, weights: Dict[str, float], rng: np.random.Generator) -> str:
names = list(weights.keys())
w = np.array([weights.get(n, 0.0) for n in names])
total = w.sum()
if total <= 0:
return rng.choice(names)
probs = w / total
return rng.choice(names, p=probs)
def _update_weights(self, d_name: str, r_name: str, reward: float):
xi = self.cfg["reaction_factor"]
self.destroy_weights[d_name] = (
1 - xi
) * self.destroy_weights.get(d_name, 1.0) + xi * reward
self.repair_weights[r_name] = (
1 - xi
) * self.repair_weights.get(r_name, 1.0) + xi * reward
def _check_aspiration(
self,
S_new: Solution,
new_cost: float,
best_cost: float,
removed_customers: List[int],
) -> bool:
"""Check aspiration criteria (Eq.29-31).
Returns True if the Tabu move should be accepted anyway.
"""
# Global best aspiration (Eq.29)
if new_cost < best_cost:
return True
# Least-frequency aspiration (Eq.30): accept if targets rarely used assignments
for c in removed_customers:
route_idx = S_new.find_route(c)
if route_idx is not None:
if self.freq_mem.is_low_frequency(c, route_idx, self.cfg["aspiration_beta"]):
return True
# Traffic adaptation aspiration (Eq.31): accept if significantly reduces congestion
# (Simplified: check congestion exposure reduction)
current_congestion = self.cost_calc.compute_congestion_exposure(S_new, self.ctx)
# We don't have the "current" solution reference here, simplified check
# Full implementation would compare to current solution
return False
def solve(self, seed: int = None) -> Solution:
"""Run T-ALNS optimization (Algorithm 2)."""
rng = np.random.default_rng(seed)
t_start = time.time()
# Initialize weights
self.destroy_weights = {name: 1.0 for name in self.destroy_ops}
self.repair_weights = {name: 1.0 for name in self.repair_ops}
# Clear tabu structures
self.move_tabu.clear()
self.sol_tabu.clear()
self.freq_mem.clear()
# Build initial solution
S_current = self._construct_initial(seed=seed)
S_best = S_current.copy()
current_cost = self.cost_calc.compute_total_cost(S_current, self.ctx)
best_cost = current_cost
# Initialize SA
initial_temp = self.cfg["initial_temperature_factor"] * max(current_cost, 1.0)
sa = SimulatedAnnealing(
initial_temp=initial_temp,
cooling_rate=self.cfg["cooling_rate"],
seed=seed,
)
n_customers = len(self.ctx.customers)
q_min = max(1, int(self.cfg["destroy_ratio_min"] * n_customers))
q_max = max(q_min + 1, int(self.cfg["destroy_ratio_max"] * n_customers))
stall_counter = 0
last_best_iter = 0
iter_count = 0
self.iteration_history = []
self.best_cost_history = []
while iter_count < self.cfg["max_iterations"]:
elapsed = time.time() - t_start
if elapsed > self.cfg["time_limit_sec"]:
break
if stall_counter >= self.cfg["stall_limit"]:
break
# Compute diversification intensity (Eq.27)
delta = self._compute_diversification_intensity(iter_count, last_best_iter)
# Determine selection probabilities
if delta > self.cfg["diversification_delta_max"]:
self._modified_d = self._modify_probabilities(delta, self.destroy_weights)
self._modified_r = self._modify_probabilities(delta, self.repair_weights)
d_weights = self._modified_d
r_weights = self._modified_r
else:
d_weights = self.destroy_weights
r_weights = self.repair_weights
# Generate candidate moves
accepted = False
for attempt in range(self.cfg["max_attempts"]):
d_name = self._select_operator(d_weights, rng)
r_name = self._select_operator(r_weights, rng)
q = rng.integers(q_min, q_max + 1)
# Apply destroy
S_temp = S_current.copy()
if d_name == "worst":
S_temp, removed = self.destroy_ops[d_name](
S_temp, self.ctx, self.cost_calc, rng, q
)
elif d_name == "related":
S_temp, removed = self.destroy_ops[d_name](
S_temp, self.ctx, rng, q
)
else:
S_temp, removed = self.destroy_ops[d_name](S_temp, rng, q)
if not removed:
continue
# Check move Tabu (Eq.23) - only if enabled
if not self.cfg.get("disable_move_tabu", False):
if self.move_tabu.is_tabu(
set(removed), d_name, r_name, iter_count
):
continue
# Apply repair
S_new = self.repair_ops[r_name](
S_temp, removed, self.ctx, self.cost_calc, rng
)
# Check solution Tabu (Eq.24) - only if enabled
if not self.cfg.get("disable_solution_tabu", False):
if self.sol_tabu.is_tabu(S_new, iter_count):
new_cost_temp = self.cost_calc.compute_total_cost(S_new, self.ctx)
if not self._check_aspiration(S_new, new_cost_temp, best_cost, removed):
continue
# Evaluate cost
new_cost = self.cost_calc.compute_total_cost(S_new, self.ctx)
# Acceptance decision
if sa.accept(current_cost, new_cost):
S_current = S_new
current_cost = new_cost
# Record in Tabu memories (respect toggles)
if not self.cfg.get("disable_move_tabu", False):
self.move_tabu.add(set(removed), d_name, r_name, iter_count)
if not self.cfg.get("disable_solution_tabu", False):
self.sol_tabu.add(S_new, iter_count)
if not self.cfg.get("disable_frequency_memory", False):
self.freq_mem.update(S_new)
if new_cost < best_cost:
S_best = S_new.copy()
best_cost = new_cost
stall_counter = 0
last_best_iter = iter_count
self.move_tabu.update_tenure(found_improvement=True)
reward = self.cfg["reward_global_best"]
else:
stall_counter += 1
self.move_tabu.update_tenure(found_improvement=False)
reward = self.cfg["reward_improvement"]
accepted = True
else:
reward = self.cfg["reward_rejected"]
# Update weights
if iter_count % self.cfg["segment_length"] == 0:
self._update_weights(d_name, r_name, reward)
break
if not accepted:
stall_counter += 1
sa.cool()
self.iteration_history.append(current_cost)
self.best_cost_history.append(best_cost)
iter_count += 1
self._iter_count = iter_count
self._best_cost = best_cost
return S_best
def run(self, seed: int = None) -> dict:
"""Run solver and return comprehensive metrics."""
t0 = time.time()
solution = self.solve(seed=seed)
elapsed = time.time() - t0
metrics = self.cost_calc.evaluate_solution(solution, self.ctx)
metrics["computation_time"] = elapsed
metrics["algorithm"] = "T-ALNS"
metrics["iterations"] = getattr(self, "_iter_count", 0)
metrics["convergence_history"] = self.best_cost_history
return metrics

View File

@@ -0,0 +1,255 @@
"""
Visualization module for T-ALNS-RRD reproduction.
Generates publication-quality figures:
Figure 1: Route map (depot + customers + routes)
Figure 2: Algorithm framework diagram
Figure 3: Cost comparison bar chart
Figure 4: Convergence curves
Figure 5: Ablation / module contribution chart
Figure 6: RRD before/after route change
"""
import numpy as np
import pandas as pd
from pathlib import Path
from typing import Dict, List, Optional
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
plt.rcParams.update({
"figure.dpi": 150,
"font.size": 11,
"axes.titlesize": 13,
"axes.labelsize": 12,
})
def plot_route_map(
solution: "Solution",
problem_ctx: "ProblemContext",
title: str = "Urban Delivery Route Map",
save_path: Optional[str] = None,
):
"""Figure 1: Route map showing depot, customers, and vehicle routes."""
fig, ax = plt.subplots(figsize=(10, 8))
depot = problem_ctx.depot
customers = problem_ctx.customers
# Plot depot
ax.scatter(depot.x_km, depot.y_km, c="red", marker="s", s=150,
edgecolors="black", linewidth=1.5, zorder=5, label="Depot")
# Color map for vehicles
colors = plt.cm.Set1(np.linspace(0, 1, solution.n_vehicles))
# Plot routes
for k, route in enumerate(solution.routes):
if len(route.nodes) < 3:
continue
nodes = route.nodes
xs, ys = [], []
for node in nodes:
if node == 0:
xs.append(depot.x_km)
ys.append(depot.y_km)
elif node in customers:
c = customers[node]
xs.append(c.x_km)
ys.append(c.y_km)
ax.plot(xs, ys, "-", color=colors[k], linewidth=2, alpha=0.7,
label=f"Vehicle {k+1} ({len(route.customers)} stops)")
# Plot customers
for cid, c in customers.items():
ax.scatter(c.x_km, c.y_km, c="white", edgecolors="black",
s=60, linewidth=0.8, zorder=4)
ax.set_xlabel("X (km)")
ax.set_ylabel("Y (km)")
ax.set_title(title)
ax.legend(loc="upper right", fontsize=9)
ax.set_xlim(0, problem_ctx.depot.x_km * 2)
ax.set_ylim(0, problem_ctx.depot.y_km * 2)
ax.set_aspect("equal")
ax.grid(True, alpha=0.3)
if save_path:
fig.savefig(save_path, bbox_inches="tight", dpi=150)
plt.close(fig)
else:
plt.show()
def plot_cost_comparison(
results_df: pd.DataFrame,
title: str = "Algorithm Performance Comparison",
save_path: Optional[str] = None,
):
"""Figure 3: Bar chart comparing Total Cost, OTDR, and CES across algorithms."""
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
algorithms = results_df["algorithm"].values
metrics = [
("total_cost_mean", "total_cost_std", "Total Cost", axes[0]),
("otdr_mean", "otdr_std", "OTDR (%)", axes[1]),
("ces_mean", "ces_std", "Congestion Exposure Score", axes[2]),
]
colors = plt.cm.viridis(np.linspace(0.2, 0.9, len(algorithms)))
for mean_col, std_col, ylabel, ax in metrics:
means = results_df[mean_col].values
stds = results_df[std_col].values
x = np.arange(len(algorithms))
bars = ax.bar(x, means, yerr=stds, color=colors, edgecolor="black",
linewidth=0.8, capsize=5)
ax.set_xticks(x)
ax.set_xticklabels(algorithms, rotation=30, ha="right", fontsize=8)
ax.set_ylabel(ylabel)
ax.set_title(ylabel)
ax.grid(True, alpha=0.3, axis="y")
# Add value labels on bars
for bar, val in zip(bars, means):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width() / 2., height,
f"{val:.1f}", ha="center", va="bottom", fontsize=7)
fig.suptitle(title, fontsize=14, fontweight="bold")
plt.tight_layout()
if save_path:
fig.savefig(save_path, bbox_inches="tight", dpi=150)
plt.close(fig)
else:
plt.show()
def plot_convergence(
convergence_data: Dict[str, List[float]],
title: str = "Convergence Curves",
save_path: Optional[str] = None,
):
"""Figure 4: Convergence curves comparing ALNS-Base vs T-ALNS vs T-ALNS-RRD."""
fig, ax = plt.subplots(figsize=(10, 6))
for label, history in convergence_data.items():
iterations = list(range(len(history)))
ax.plot(iterations, history, linewidth=1.5, alpha=0.7, label=label)
ax.set_xlabel("Iterations")
ax.set_ylabel("Best Cost")
ax.set_title(title)
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3)
if save_path:
fig.savefig(save_path, bbox_inches="tight", dpi=150)
plt.close(fig)
else:
plt.show()
def plot_ablation(
ablation_df: pd.DataFrame,
title: str = "Component Ablation Study",
save_path: Optional[str] = None,
):
"""Figure 5: Horizontal bar chart showing incremental contribution of each module."""
fig, ax = plt.subplots(figsize=(10, 6))
configs = ablation_df["configuration"].values
costs = ablation_df["total_cost_mean"].values
costs_std = ablation_df["total_cost_std"].values
# Sort by cost (descending)
idx = np.argsort(costs)[::-1]
configs = configs[idx]
costs = costs[idx]
costs_std = costs_std[idx]
colors = plt.cm.RdYlGn_r(np.linspace(0.2, 0.9, len(configs)))
bars = ax.barh(range(len(configs)), costs, xerr=costs_std,
color=colors, edgecolor="black", linewidth=0.8, capsize=3)
ax.set_yticks(range(len(configs)))
ax.set_yticklabels(configs)
ax.set_xlabel("Total Cost")
ax.set_title(title)
ax.grid(True, alpha=0.3, axis="x")
ax.invert_yaxis()
# Add difference labels
for i in range(len(costs) - 1):
diff = costs[i] - costs[i + 1]
mid_y = i + 0.5
ax.annotate(f"-{diff:.1f}", xy=(costs[i], mid_y),
fontsize=7, ha="right", color="darkred")
plt.tight_layout()
if save_path:
fig.savefig(save_path, bbox_inches="tight", dpi=150)
plt.close(fig)
else:
plt.show()
def plot_rrd_comparison(
before_solution: "Solution",
after_solution: "Solution",
problem_ctx: "ProblemContext",
event_info: str = "",
save_path: Optional[str] = None,
):
"""Figure 6: Before/after route change visualization for RRD event."""
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
depot = problem_ctx.depot
customers = problem_ctx.customers
colors = plt.cm.Set1(np.linspace(0, 1, problem_ctx.n_vehicles))
for ax, sol, label in [(ax1, before_solution, "Before Dispatch"),
(ax2, after_solution, "After Dispatch")]:
ax.scatter(depot.x_km, depot.y_km, c="red", marker="s", s=120,
edgecolors="black", zorder=5, label="Depot")
for k, route in enumerate(sol.routes):
if len(route.nodes) < 3:
continue
xs, ys = [], []
for node in route.nodes:
if node == 0:
xs.append(depot.x_km)
ys.append(depot.y_km)
elif node in customers:
xs.append(customers[node].x_km)
ys.append(customers[node].y_km)
ax.plot(xs, ys, "-", color=colors[k], linewidth=2, alpha=0.7)
for cid, c in customers.items():
ax.scatter(c.x_km, c.y_km, c="white", edgecolors="black", s=40)
ax.set_title(label, fontweight="bold")
ax.set_xlabel("X (km)")
ax.set_ylabel("Y (km)")
ax.set_xlim(0, depot.x_km * 2)
ax.set_ylim(0, depot.y_km * 2)
ax.set_aspect("equal")
ax.grid(True, alpha=0.3)
fig.suptitle(f"RRD Event Response: {event_info}", fontsize=13, fontweight="bold")
plt.tight_layout()
if save_path:
fig.savefig(save_path, bbox_inches="tight", dpi=150)
plt.close(fig)
else:
plt.show()

775
work_plan.md Normal file
View File

@@ -0,0 +1,775 @@
可以,而且**非常建议你这样做**。在作者暂时没有提供原始数据集的情况下,你完全可以做 **“基于自定义合成数据集的方法复现”**,并比较不同算法模块的效果。
但要注意表述:这不是“完全复现原文数值结果”,而是 **methodological reproduction / algorithmic reproduction**,也就是复现论文的核心算法流程和实验对比逻辑。原文实验本身也是一个受控的合成城市配送场景,作者使用了 47 个客户、4 辆车、上海中心城区启发的路网和合成订单需求,并明确说这是 proof-of-concept validation而不是直接代表真实运营部署。
下面给你一份可以直接作为项目开发依据的 **复现方案文档初稿**
---
# T-ALNS-RRD 论文复现方案文档
## 1. 复现目标
本项目目标是在无法获得原文完整数据集的情况下,构建一个与论文设定相近的自定义合成城市末端配送数据集,并复现论文提出的核心算法框架:
[
Static\text{-}VRPTW
\rightarrow
TA\text{-}VRPTW\text{-}Greedy
\rightarrow
ALNS
\rightarrow
T\text{-}ALNS
\rightarrow
T\text{-}ALNS\text{-}RRD
]
重点不是追求和论文完全一致的数值,而是验证以下趋势:
1. 加入交通感知成本后,路径选择能减少拥堵暴露;
2. ALNS 相比贪婪算法能进一步降低总成本;
3. Tabu memory 能减少重复搜索,提高解的稳定性;
4. RRD 实时调度能在突发事件下减少延误和服务失败;
5. 各模块叠加后,整体表现应优于静态路径规划。
论文的核心方法本身就是将动态拥堵惩罚 ALNS、多层 Tabu memory 和 rollout 实时调度集成到统一框架中。
---
## 2. 复现类型说明
本复现属于:
**基于合成数据的算法机制复现**
不是:
**基于原始作者数据的严格结果复现**
建议你在汇报或报告中这样写:
> 由于原文数据集需向作者合理请求,且目前尚未获得完整数据与代码,本项目采用与论文实验规模和数据结构相近的自定义合成数据集,复现其核心算法流程和对比实验框架。复现重点在于验证不同算法模块对总成本、准时率、拥堵暴露和实时扰动响应能力的相对影响,而非逐项复刻原文数值结果。
这句话很重要,可以避免老师质疑“为什么你的结果和论文百分比不完全一样”。
---
## 3. 自定义数据集设计
### 3.1 基本规模
参考原文实验设置,建议设置:
| 项目 | 复现设置 |
| ----- | ------------------- |
| 客户数 | 47 |
| 仓库数 | 1 |
| 车辆数 | 4 |
| 车辆容量 | 120 kg |
| 服务时间 | 每个客户 4 min |
| 时间窗 | 上午、下午、傍晚三类 |
| 运营时间 | 6:0018:00 |
| 时间段数量 | 12 个,每段 1 小时 |
| 区域范围 | 8 km × 10 km 合成城市区域 |
原文实验也是 47 个客户、4 辆车,客户需求为 312 kg服务时间为 4 分钟,时间窗分为 morning、afternoon、evening 三个时段。
---
### 3.2 客户数据生成
每个客户包含:
```text
customer_id
x
y
demand
service_time
earliest_time
latest_time
priority
```
建议生成规则:
```text
x ∈ [0, 8] km
y ∈ [0, 10] km
demand ∈ [3, 12] kg
service_time = 4 min
time window:
morning: 9:0012:00
afternoon: 13:0016:00
evening: 17:0020:00
```
为了更像城市订单,不建议完全均匀随机分布。可以生成 3 个客户簇:
```text
住宅区簇
商业区簇
办公区簇
```
这样路线优化会更有意义。
---
### 3.3 路网与弧数据生成
如果不使用真实 OSM可以先用完全图简化
[
A = N \times N, i \neq j
]
每两个节点之间都有一条可行弧。
每条弧包含:
```text
from_node
to_node
distance
base_travel_time
road_type
```
距离用欧氏距离:
[
d_{ij}=\sqrt{(x_i-x_j)^2+(y_i-y_j)^2}
]
基础行驶时间:
[
base_time_{ij}=\frac{distance_{ij}}{speed}
]
可设置三类道路:
| road_type | speed |
| ----------- | ------- |
| arterial | 45 km/h |
| collector | 30 km/h |
| residential | 20 km/h |
课程复现阶段,完全图已经足够;后续想更高级,可以再用 `networkx` 生成网格路网。
---
### 3.4 时间依赖交通矩阵
论文中交通流数据被离散为多个时间区间,并为每条弧计算时间依赖行驶时间和拥堵权重。 复现中建议生成三个张量:
```text
travel_time[i, j, h]
congestion[i, j, h]
uncertainty[i, j, h]
```
其中:
```text
i, j: 节点编号
h: 时间段编号011
```
交通拥堵可以用时间段乘子表示:
| 时间段 | 拥堵强度 |
| ----------- | ---- |
| 6:007:00 | 1.0 |
| 7:009:00 | 1.6 |
| 9:0011:00 | 1.2 |
| 11:0013:00 | 1.0 |
| 13:0016:00 | 1.2 |
| 16:0018:00 | 1.7 |
行驶时间:
[
t_{ij}^{(h)} = base_time_{ij} \times traffic_multiplier_h \times road_noise
]
拥堵权重:
[
\gamma_{ij}^{(h)} \in [0,1]
]
可靠性不确定性:
[
\eta_{ij}^{(h)} = \sigma \cdot t_{ij}^{(h)}
]
风险调整时间:
[
t'*{ij}^{(h)} = t*{ij}^{(h)} + \beta \eta_{ij}^{(h)}
]
---
## 4. 需要复现的算法
### 4.1 Baseline 1Static-VRPTW
静态车辆路径问题。
特点:
```text
不考虑时间变化
不考虑拥堵惩罚
只使用固定 base_travel_time
使用 greedy insertion 生成路线
```
目标:
作为最基础对照组。
---
### 4.2 Baseline 2TA-VRPTW-Greedy
交通感知贪婪算法。
特点:
```text
考虑 time-dependent travel time
考虑 congestion penalty
但不使用 ALNS
```
目标:
验证“交通感知成本”本身能否带来改进。
---
### 4.3 Baseline 3ALNS-Base
复现论文的基础 ALNS。
核心组件:
```text
初始解greedy insertion
destroy operators:
random removal
worst removal
relatedness removal
repair operators:
greedy insertion
regret-2 insertion
time-window-aware insertion
acceptance:
simulated annealing
operator selection:
adaptive weights
```
论文中 ALNS 通过 destroy-repair 循环生成候选解,并使用自适应算子权重和模拟退火接受准则。
---
### 4.4 Baseline 4T-ALNS
在 ALNS 上加入 Tabu memory。
建议先实现三个版本:
```text
T-ALNS-Move:
只加入 move-based tabu
T-ALNS-Solution:
加入 solution hash memory
T-ALNS-Full:
加入 move tabu + solution tabu + frequency memory
```
最小实现:
```text
move-based tabu:
记录最近被移除客户集合和算子组合
solution-based tabu:
记录最近路线结构 hash
frequency memory:
记录 customer-vehicle 分配频率
```
目标:
验证 Tabu 是否能进一步降低成本、提高稳定性。
---
### 4.5 ProposedT-ALNS-RRD
在 T-ALNS 上加入简化版实时调度。
事件类型:
| 事件 | 复现方式 |
| ------------------ | ------------------------ |
| Traffic incident | 某条边 travel_time 临时乘以 23 |
| Urgent order | 中途新增一个客户 |
| Capacity violation | 某客户需求临时增加 |
| Time-window risk | 预测某客户即将迟到 |
候选动作:
```text
Action A: local reroute
Action B: customer reassignment
Action C: service postponement
```
Rollout 评估:
```text
对每个候选动作,模拟未来 60 min
计算 expected cost
加入 route stability penalty
选择 adjusted cost 最低的动作
```
论文中 RRD 的基本思想就是在突发事件下生成候选响应动作,并通过 bounded-horizon rollout simulation 选择响应方案。
---
## 5. 目标函数与指标
### 5.1 总成本函数
复现目标函数建议使用:
[
Cost =
TravelTime
+
\lambda_1 LatePenalty
+
\lambda_2 CongestionPenalty
+
\lambda_3 StabilityPenalty
]
其中:
```text
TravelTime: 总行驶时间
LatePenalty: 所有客户迟到时间之和
CongestionPenalty: 路径拥堵暴露之和
StabilityPenalty: 实时调度后路线变化程度
```
对于非 RRD 算法,(\lambda_3=0)。
---
### 5.2 主要评价指标
| 指标 | 含义 |
| ------------------ | ------------------------- |
| Total Cost | 综合运营成本 |
| Travel Time Cost | 总行驶时间 |
| Delay Penalty | 迟到惩罚 |
| Congestion Cost | 拥堵成本 |
| OTDR | 准时送达率 |
| Average Delay | 平均迟到时间 |
| Max Delay | 最大迟到时间 |
| Late Customers | 迟到客户数 |
| CES | Congestion Exposure Score |
| Computation Time | 算法运行时间 |
| Iterations to Best | 找到最优解的迭代次数 |
| Event Success Rate | RRD 事件响应成功率 |
| Response Time | RRD 平均响应时间 |
原文也使用了 total cost、OTDR、CES、实时重调度响应、计算时间等指标进行评估。
---
## 6. 实验设计
### 实验一:主对比实验
比较:
```text
Static-VRPTW
TA-VRPTW-Greedy
ALNS-Base
T-ALNS
T-ALNS-RRD
```
每个算法运行:
```text
30 个随机种子
max_iter = 1000
time_limit = 600 s可根据电脑性能缩短
```
输出表格:
```text
Algorithm | Total Cost | OTDR | CES | Computation Time
```
预期结果趋势:
```text
Static-VRPTW 成本最高、准时率最低
TA-VRPTW-Greedy 拥堵暴露下降
ALNS-Base 总成本进一步下降
T-ALNS 稳定性更好
T-ALNS-RRD 在动态扰动下表现最好
```
---
### 实验二:消融实验
目的:证明各模块确实有贡献。
配置:
```text
ALNS-Base
+ Move Tabu
+ Solution Tabu
+ Frequency Memory
Full T-ALNS
Full T-ALNS + RRD
```
输出:
```text
Total Cost
OTDR
CES
Convergence Time
```
---
### 实验三:交通不确定性鲁棒性实验
设置不同交通扰动强度:
```text
sigma = 0.1
sigma = 0.2
sigma = 0.3
sigma = 0.5
```
每组重复 30 次。
输出:
```text
Algorithm | Low | Medium | High | Extreme | Degradation Rate
```
预期结果:
```text
T-ALNS-RRD 随 sigma 增大退化最慢
Static-VRPTW 退化最快
```
原文也做了 varying traffic uncertainty levels 的鲁棒性测试。
---
### 实验四:参数敏感性实验
参考原文做三个维度:
| 参数 | 取值 |
| ---- | ---------------------- |
| 客户数 | 30, 40, 47, 60 |
| 车辆数 | 2, 3, 4, 5, 6 |
| 车辆容量 | 80, 100, 120, 140, 160 |
输出:
```text
不同参数下的 Total Cost、OTDR、CES、Computation Time
```
作用:
说明算法不是只在某个单一设置下有效。
---
### 实验五:实时事件响应实验
模拟事件:
```text
rush hour congestion
random accident
urgent order
road closure
combined stress
```
输出:
```text
Event Type | Frequency | Success Rate | Cost Reduction | Response Time
```
如果时间有限,可以只做三类:
```text
traffic incident
urgent order
time-window risk
```
---
## 7. 项目代码结构建议
建议让 opencode 按下面结构生成项目:
```text
t_alns_rrd_reproduction/
├── README.md
├── requirements.txt
├── configs/
│ ├── default.yaml
│ ├── experiment_main.yaml
│ ├── experiment_ablation.yaml
│ └── experiment_robustness.yaml
├── data/
│ ├── synthetic/
│ │ ├── customers.csv
│ │ ├── depot.csv
│ │ ├── vehicles.csv
│ │ ├── arcs.csv
│ │ ├── travel_time.npy
│ │ ├── congestion.npy
│ │ └── uncertainty.npy
├── src/
│ ├── data_generator.py
│ ├── problem.py
│ ├── cost.py
│ ├── schedule.py
│ ├── baselines/
│ │ ├── static_vrptw.py
│ │ └── ta_greedy.py
│ ├── alns/
│ │ ├── operators_destroy.py
│ │ ├── operators_repair.py
│ │ ├── alns_base.py
│ │ └── acceptance.py
│ ├── tabu/
│ │ ├── move_tabu.py
│ │ ├── solution_tabu.py
│ │ └── frequency_memory.py
│ ├── rrd/
│ │ ├── event_generator.py
│ │ ├── candidate_actions.py
│ │ ├── rollout.py
│ │ └── dispatch.py
│ ├── experiments/
│ │ ├── run_main_comparison.py
│ │ ├── run_ablation.py
│ │ ├── run_robustness.py
│ │ └── run_sensitivity.py
│ └── visualization/
│ ├── plot_routes.py
│ ├── plot_convergence.py
│ └── plot_results.py
├── results/
│ ├── tables/
│ ├── figures/
│ └── logs/
└── report/
├── reproduction_notes.md
└── figures_for_ppt/
```
---
## 8. 最小可行复现版本
如果时间比较紧,不要一次性追求完整 T-ALNS-RRD。建议按三阶段推进。
### 阶段 1基础可运行
实现:
```text
data_generator
cost function
Static-VRPTW
TA-VRPTW-Greedy
```
输出:
```text
两种算法的 total cost、OTDR、CES
```
### 阶段 2核心算法复现
实现:
```text
ALNS-Base
T-ALNS
convergence plot
ablation table
```
这一步是最重要的。
### 阶段 3实时调度增强
实现:
```text
event_generator
candidate actions
simplified rollout
T-ALNS-RRD
```
输出:
```text
事件响应成功率
扰动前后成本变化
动态路线变化图
```
---
## 9. 建议绘制的图表
你最终汇报可以准备 6 张图:
| 图 | 内容 |
| -------- | ------------------------------ |
| Figure 1 | 合成城市配送场景:仓库、客户、路线 |
| Figure 2 | 算法框架图ALNS → Tabu → RRD |
| Figure 3 | 主实验柱状图Total Cost / OTDR / CES |
| Figure 4 | 收敛曲线ALNS vs T-ALNS |
| Figure 5 | 消融实验图:不同模块贡献 |
| Figure 6 | RRD 示例图:突发拥堵前后路线变化 |
其中 Figure 2 和 Figure 6 最适合用在 PPT 中讲清楚论文贡献。
---
## 10. 给 opencode 的开发提示词
你可以直接把下面这段发给 opencode
```text
请帮我构建一个 Python 项目,用于复现论文 “Optimizing urban last mile delivery efficiency through dynamic vehicle routing heuristics and traffic flow analysis” 中的核心算法思想。由于原文数据集暂未获得,请使用合成数据集进行算法机制复现。
项目目标:
1. 构建 47 个客户、1 个仓库、4 辆车的城市末端配送合成数据集;
2. 每个客户包含坐标、需求量、服务时间、时间窗;
3. 构建时间依赖交通矩阵,包括 travel_time[i,j,h]、congestion[i,j,h]、uncertainty[i,j,h],其中 h=0,...,11
4. 实现以下算法:
- Static-VRPTW greedy baseline
- Traffic-aware VRPTW greedy baseline
- ALNS-Base
- T-ALNS with move tabu, solution tabu, frequency memory
- Simplified T-ALNS-RRD with event generation and rollout dispatch
5. 实现统一成本函数:
total_cost = travel_time + lambda_delay * lateness + lambda_congestion * congestion + lambda_stability * route_change_penalty
6. 输出评价指标:
total cost, travel time, delay penalty, congestion cost, OTDR, average delay, max delay, late customers, CES, computation time, convergence curve, event response success rate
7. 实现实验脚本:
- main comparison experiment
- ablation experiment
- traffic uncertainty robustness experiment
- parameter sensitivity experiment
8. 生成可视化图:
- route map
- cost comparison bar chart
- OTDR comparison chart
- CES comparison chart
- convergence curves
- RRD event before/after route change plot
代码要求:
- 使用 Python 3.11
- 使用 numpy, pandas, matplotlib, networkx, pyyaml
- 项目结构清晰,模块化组织
- 所有随机过程必须支持 random_seed
- 每个算法类都提供 run() 方法
- 实验结果保存到 results/tables 和 results/figures
- README 中说明如何运行每个实验
```
---
## 11. 成功标准
你的复现项目不需要得到和论文完全相同的 24.3%、92.8%、54.4% 这些数值,因为数据不同。
你的成功标准应该是:
```text
1. 能生成可重复的合成数据集;
2. 能运行至少 4 个算法版本;
3. 能输出统一评价指标;
4. 能展示从 Static → TA-Greedy → ALNS → T-ALNS → T-ALNS-RRD 的逐步改进趋势;
5. 能在报告中解释每个模块为什么带来性能变化;
6. 能说明该复现是基于自定义数据的算法机制复现。
```
---
# 最推荐的复现策略
我建议你先不要完整追求 RRD而是优先完成
[
Static
\rightarrow
TA\text{-}Greedy
\rightarrow
ALNS
\rightarrow
T\text{-}ALNS
]
这四个版本已经足够支撑课程汇报中的“算法复现”。RRD 可以作为增强实验,用一个简化交通事故案例展示即可。
最终汇报时,你可以这样定位你的工作:
> 本项目在原文数据集暂未公开的条件下构建了与论文实验规模一致的合成城市末端配送场景并复现了论文的核心算法链条。实验通过逐步引入交通感知成本、ALNS 搜索、Tabu 记忆机制和简化实时调度模块,对比不同算法配置在总成本、准时率和拥堵暴露上的表现,从而验证原文 T-ALNS-RRD 框架的算法设计逻辑。