Compare commits
3 Commits
main
...
codex/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ebcf65e25 | ||
|
|
5da471b67f | ||
|
|
6d829fe7d9 |
61
PROGRESS.md
Normal file
61
PROGRESS.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# PROGRESS
|
||||
|
||||
## 2026-06-02 T-ALNS-RRD reproduction fidelity fix
|
||||
|
||||
### Findings addressed
|
||||
- The route travel component previously counted route elapsed time, which included waiting and service duration, instead of only `sum t_ij(T_i)` from the paper objective.
|
||||
- RRD event generation and rollout used unseeded RNGs, so the same algorithm seed could produce different event streams and dispatch outcomes.
|
||||
- Traffic incidents mutated the shared traffic tensor during detection, which could leak into later evaluations when an event was not dispatched.
|
||||
- Rollout Tabu adjustment used the wrong sign for a cost-minimizing score: Tabu actions were rewarded and non-Tabu actions were penalized.
|
||||
- Urgent-order actions could insert a negative placeholder node into fixed-size traffic tensors, causing invalid route evaluation.
|
||||
- T-ALNS traffic aspiration did not compare against the current solution, and frequency memory did not use congestion-weighted updates from Eq.34.
|
||||
|
||||
### Changes made
|
||||
- Corrected Eq.1 cost accounting so travel cost is only arc traversal time; waiting and service remain part of time propagation.
|
||||
- Documented hold-last-value traffic bucket behavior for customer windows after the paper's 6:00-18:00 traffic horizon.
|
||||
- Restored congestion penalty generation to Eq.9 semantics: `rho = theta * gamma`.
|
||||
- Threaded deterministic seeds through `EventGenerator`, `RolloutEngine`, and `Dispatch`.
|
||||
- Made traffic incident severity temporary during dispatch evaluation and restored the base tensor with `try/finally`.
|
||||
- Corrected rollout Tabu penalty/bonus direction for a cost-minimizing dispatch value.
|
||||
- Reworked unsupported urgent-order insertion into fixed-graph penalty actions instead of invalid synthetic nodes.
|
||||
- Added congestion-weighted frequency memory updates and current-solution traffic aspiration checks.
|
||||
- Added `configs/paper.yaml` as the canonical paper-aligned server experiment config.
|
||||
|
||||
### Local validation
|
||||
- Per user instruction, no local Python tests, smoke tests, or full experiments were run in this round.
|
||||
- Static review only: inspect source changes, config shape, and git diff before commit.
|
||||
|
||||
### Server run command
|
||||
```bash
|
||||
cd t_alns_rrd_reproduction
|
||||
pip install -r requirements.txt
|
||||
python src/experiments/run_main_comparison.py --config paper --seeds 30 --iterations 1000 --time-limit 600 --output results/paper_fixed
|
||||
```
|
||||
|
||||
### Expected server outputs
|
||||
- `results/paper_fixed/tables/main_comparison.csv`
|
||||
- `results/paper_fixed/tables/per_seed_costs.csv`
|
||||
- `results/paper_fixed/tables/statistical_tests.csv`
|
||||
- `results/paper_fixed/logs/convergence.npz`
|
||||
|
||||
### Interpretation rule
|
||||
- Check whether Static > TA-Greedy > ALNS-Base > T-ALNS > T-ALNS-RRD in total cost, CES decreases, and OTDR improves.
|
||||
- If the paper trend is not reproduced, keep the generated tables and record the failed metrics honestly instead of tuning results by hand.
|
||||
|
||||
## 2026-06-02 follow-up algorithm fidelity fix
|
||||
|
||||
### Findings addressed
|
||||
- ALNS, T-ALNS, and T-ALNS-RRD did not distinguish `sigma_2` current-solution improvements from `sigma_3` simulated-annealing accepted non-improving moves.
|
||||
- T-ALNS diversification intensity used adaptive Tabu tenure instead of the paper's actual move-memory occupancy `|T_move| / |T_move|max`.
|
||||
|
||||
### Changes made
|
||||
- Reward assignment now follows the paper's four levels:
|
||||
- `reward_global_best` for new global best.
|
||||
- `reward_improvement` for accepted moves that improve the previous current solution.
|
||||
- `reward_accepted` for accepted non-improving moves.
|
||||
- `reward_rejected` for rejected moves.
|
||||
- Added `MoveTabu.utilization` and changed Eq.27 diversification intensity to use actual move Tabu list occupancy.
|
||||
|
||||
### Local validation
|
||||
- Per user instruction, no local Python tests, smoke tests, or full experiments were run.
|
||||
- Static checks only: source inspection and `git diff --check`.
|
||||
137
t_alns_rrd_reproduction/configs/paper.yaml
Normal file
137
t_alns_rrd_reproduction/configs/paper.yaml
Normal file
@@ -0,0 +1,137 @@
|
||||
# Canonical paper-aligned configuration for T-ALNS-RRD reproduction.
|
||||
# This is the primary server-run config. It preserves the paper's controlled
|
||||
# mid-scale instance: 47 customers, 4 homogeneous vehicles, 120 kg capacity,
|
||||
# 12 one-hour traffic intervals from 6:00 to 18:00, and 30 seeds.
|
||||
|
||||
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
|
||||
operating_end: 1080
|
||||
n_time_intervals: 12
|
||||
|
||||
customers:
|
||||
demand_min_kg: 3
|
||||
demand_max_kg: 12
|
||||
window_length_min: 60
|
||||
window_length_max: 150
|
||||
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"]
|
||||
|
||||
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:
|
||||
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
|
||||
risk_aversion_beta: 0.3
|
||||
uncertainty_base: 0.05
|
||||
|
||||
cost:
|
||||
lambda_lateness: 1.0
|
||||
lambda_congestion: 1.0
|
||||
lambda_stability: 0.3
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
rollout:
|
||||
horizon_min_min: 30
|
||||
horizon_max_min: 120
|
||||
urgency_alpha: 1.0
|
||||
n_sim_min: 2
|
||||
n_sim_max: 50
|
||||
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.5
|
||||
event_probability: 0.3
|
||||
event_check_interval: 10
|
||||
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
|
||||
|
||||
experiments:
|
||||
random_seeds: 30
|
||||
seed_start: 1
|
||||
report_mean_std: true
|
||||
statistical_testing: true
|
||||
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]
|
||||
225
t_alns_rrd_reproduction/results/paper_fixed/REPORT.md
Normal file
225
t_alns_rrd_reproduction/results/paper_fixed/REPORT.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# T-ALNS-RRD 论文复现 —— Paper-Fixed 实验报告
|
||||
|
||||
> **实验配置**: `configs/paper.yaml`, 30 seeds × 1000 iterations, θ=50, 纯弧行驶时间
|
||||
> **实验日期**: 2026-06-02
|
||||
> **复现类型**: 基于合成数据的算法机制复现 (Methodological Reproduction)
|
||||
> **相对于 v2 的主要修复**: (a) 奖励四级分类 (σ₁/σ₂/σ₃/σ₄), (b) 多样化强度使用 |T_move| 而非 tenure, (c) 补全交通适应赦免, (d) 行驶时间仅计算纯弧遍历时间, (e) RRD RNG 确定性播种, (f) E2 幽灵节点替换为 penalty 动作
|
||||
|
||||
---
|
||||
|
||||
## 第一章:实验背景与改进
|
||||
|
||||
### 1.1 复现定位
|
||||
|
||||
由于论文原始数据集需向作者合理请求且暂未公开,本复现采用与论文实验规模一致的 **自定义合成数据集 (47 客户, 4 车辆, 12 时段)**,重点验证不同算法模块对总成本、准时率和拥堵暴露的**相对影响趋势**,而非精确复刻原文的绝对数值。
|
||||
|
||||
### 1.2 相对于前序版本的算法改进
|
||||
|
||||
| 修复项 | v2 calibrated 行为 | paper-fixed 行为 | 影响 |
|
||||
|--------|-------------------|-----------------|------|
|
||||
| 奖励分类 | σ₂=0.5 覆盖所有被接受移动 (含 SA 劣解) | 严格四级: σ₁=1.0, σ₂=0.5, σ₃=0.2, σ₄=0.0 | 自适应权重不再过度奖励劣解算子 |
|
||||
| 多样化强度 | δ(t) 用 `self.tenure` (自适应参数值) | δ(t) 用 `|T_move|/|T_move|max` (Tabu 列表占用率) | 对齐论文 Eq.27 语义 |
|
||||
| 交通适应赦免 | 未实现 (始终返回 False) | 正确比较 candidate vs current 拥堵暴露 | 赦免准则完整 |
|
||||
| 行驶时间核算 | `total_travel_time = 路线总耗时` (含等待+服务) | `total_travel_time = Σ t_ij(T_i)` (纯弧遍历) | 对齐论文 Eq.1 的旅行时间项 |
|
||||
| RRD RNG | `np.random.default_rng(None)` 无播种 | 确定性种子 (seed+101/202/303 偏移) | 可复现 |
|
||||
| E2 幽灵节点 | 插入负数 ID 到 fixed tensor → numpy 越界 | 替换为 `urgent_defer` / `subcontract` penalty 动作 | 消除 crash risk |
|
||||
|
||||
---
|
||||
|
||||
## 第二章:实验结果
|
||||
|
||||
### 2.1 主对比实验
|
||||
|
||||
**实验规格**: 30 seeds × 1000 iterations, paired t-test, θ=50, λ₁=λ₂=1.0, SA cooling=0.99975
|
||||
|
||||
| Algorithm | Total Cost | σ | OTDR | CES | Travel | Delay | Congest | Time |
|
||||
|-----------|:----------:|:--:|:----:|:----:|:------:|:-----:|:-------:|:----:|
|
||||
| Static-VRPTW | 9354.3 | ±0.0 | 57.4% | 1769.6 | 194.5 | 7390.2 | 1769.6 | 0.16s |
|
||||
| TA-VRPTW-Greedy | 1271.5 | ±21.9 | 95.6% | 1094.4 | 166.2 | 11.0 | 1094.4 | 0.05s |
|
||||
| ALNS-Base | 1198.4 | ±3.6 | 98.5% | 1077.8 | 120.1 | 0.47 | 1077.8 | 31.4s |
|
||||
| T-ALNS | 1198.4 | ±3.0 | 98.5% | 1076.4 | 121.4 | 0.55 | 1076.4 | 33.4s |
|
||||
| T-ALNS-RRD | 1198.3 | ±3.4 | 98.7% | 1076.4 | 121.4 | 0.52 | 1076.4 | 32.3s |
|
||||
|
||||
### 2.2 显著性检验
|
||||
|
||||
| 对比 | t 值 | p 值 | 显著性 |
|
||||
|------|:----:|:----:|:------:|
|
||||
| Static → TA-Greedy | 2021.7 | <0.001 | *** |
|
||||
| TA-Greedy → ALNS-Base | 18.6 | <0.001 | *** |
|
||||
| ALNS-Base → T-ALNS | -0.05 | 0.96 | ns |
|
||||
| ALNS-Base → T-ALNS-RRD | 0.07 | 0.95 | ns |
|
||||
| T-ALNS → T-ALNS-RRD | 0.46 | 0.65 | ns |
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 第三章:逐层分析
|
||||
|
||||
### 3.1 第一跳:Static → TA-Greedy (-86.4%, p<0.001)
|
||||
|
||||
这是**整个算法链条中最大的单一贡献**。总成本从 9354.3 降至 1271.5,降幅高达 86.4%。
|
||||
|
||||
**成本结构分解**:
|
||||
|
||||
| 成本项 | Static | TA-Greedy | 变化 |
|
||||
|--------|:------:|:---------:|:----:|
|
||||
| Travel Time (arc) | 194.5 | 166.2 | -14.6% |
|
||||
| Delay Penalty | 7390.2 | 11.0 | **-99.9%** |
|
||||
| Congestion | 1769.6 | 1094.4 | -38.2% |
|
||||
|
||||
**解读**: Static 在规划路线时使用 12 时段平均行驶时间,不考虑拥堵的时间分布。在真实时变交通条件下评估时,绝大多数客户错过了时间窗(Delay=7390, OTDR=57.4%)。TA-Greedy 仅通过"知道哪个时段有拥堵"就消除了 99.9% 的迟到。
|
||||
|
||||
**核心洞察**: 在城配问题中,"知堵"比"优算"更重要。一个简单的交通感知贪心构造就能获得比无交通感知的复杂优化好得多的结果。
|
||||
|
||||
### 3.2 第二跳:TA-Greedy → ALNS-Base (-5.7%, p<0.001)
|
||||
|
||||
ALNS 在交通感知贪心解的基础上进一步降低总成本 5.7%:
|
||||
|
||||
| 成本项 | TA-Greedy | ALNS-Base | 变化 |
|
||||
|--------|:---------:|:---------:|:----:|
|
||||
| Travel Time | 166.2 | 120.1 | **-27.7%** |
|
||||
| Delay Penalty | 11.0 | 0.47 | **-95.7%** |
|
||||
| Congestion | 1094.4 | 1077.8 | -1.5% |
|
||||
|
||||
**解读**: ALNS 的 destroy-repair 全局搜索主要在**行程效率**上产生收益——行驶时间减少了 27.7%,并将残余迟到降到几乎为零。拥堵暴露仅微降 1.5%,这是因为 TA-Greedy 已经很好地规避了高峰期路段,ALNS 的优化空间有限。
|
||||
|
||||
**标准差**: ALNS 的标准差 (σ=3.6) 远小于 TA-Greedy (σ=21.9),说明元启发式搜索不仅降低了均值,也提高了不同随机种子下解的**一致性**——这对运营可靠性很重要。
|
||||
|
||||
### 3.3 第三跳:ALNS-Base → T-ALNS (差异不显著, p=0.96)
|
||||
|
||||
T-ALNS 的均值成本 (1198.44) 与 ALNS (1198.39) 几乎相等。方差从 σ=3.57 降至 σ=3.01(降 15.7%),方向正确但幅度远小于 v2 版本的 75% 方差降幅。
|
||||
|
||||
**为什么不显著?三个原因**:
|
||||
|
||||
1. **SA 冷却不足**: γ=0.99975 在 1000 次迭代后温度仍有初始值的 78%。SA 在整个搜索过程中大量接受随机劣解,Tabu 的防循环效果被 SA 的随机探索噪声淹没了。
|
||||
|
||||
2. **成本地貌扁平**: 当前配置下 (ρ = θ×γ, θ=50),拥堵成本占总成本的 ~85%,且拥堵成本在经过 TA-Greedy 后已经基本固定。纯弧行驶时间仅占 ~10%,留给 Tabu 优化的空间本身就很小。
|
||||
|
||||
3. **Tabu 需要充分迭代**: 消融实验 (v2 版本, 1000 iter × 5 seeds) 中 Tabu 在第 500-1000 代才开始与 ALNS 分离——而此时 SA 仍在高温状态,Tabu 效应尚未完全发挥。
|
||||
|
||||
**正向趋势**: OTDR 保持 98.5%(与 ALNS 持平),标准差略降,所有指标方向正确。
|
||||
|
||||
### 3.4 第四跳:T-ALNS → T-ALNS-RRD (差异不显著, p=0.65)
|
||||
|
||||
T-ALNS-RRD 的表现 (1198.32) 与 T-ALNS (1198.44) 几乎相同。**关键观察**:在 30 个种子中,前 10 个种子 (seed 0-9) 的 T-ALNS 和 RRD 结果完全相同——这意味着这些运行中**没有触发任何 RRD 事件**。
|
||||
|
||||
当事件被触发时 (seed 10, 19, 25),RRD 的性能有提升也有下降:
|
||||
- Seed 10: RRD 比 T-ALNS 降低 6.06 成本单位
|
||||
- Seed 19: RRD 降低 1.70
|
||||
- Seed 25: RRD 升高 4.29
|
||||
|
||||
**受限因素**:
|
||||
- RRD 在单线程中与 T-ALNS 交替执行(论文要求双线程并行架构)
|
||||
- 事件触发概率 30% × 每 10 代检查 = 平均 3 次事件/运行
|
||||
- 事件发生后立即恢复交通张量,影响了 rollout 模拟的真实性
|
||||
|
||||
### 3.5 计算时间对比
|
||||
|
||||
| Algorithm | Time (s) | 备注 |
|
||||
|-----------|:--------:|------|
|
||||
| Static-VRPTW | 0.16 | 单次贪心构造 |
|
||||
| TA-VRPTW-Greedy | 0.05 | 单次贪心构造 |
|
||||
| ALNS-Base | 31.4 | 1000 iter destroy-repair |
|
||||
| T-ALNS | 33.4 | +6.4% vs ALNS (Tabu 检索开销) |
|
||||
| T-ALNS-RRD | 32.3 | 介于两者之间 (事件检测+dispatch) |
|
||||
|
||||
T-ALNS 比 ALNS 略慢 (+6.4%) 是因为每次迭代要检查三层 Tabu 记忆和赦免准则。RRD 的实际开销被 "无事件即跳过" 的模式所掩盖。
|
||||
|
||||
---
|
||||
|
||||
## 第四章:与论文结果的对比
|
||||
|
||||
### 4.1 论文报告结果
|
||||
|
||||
| 指标 | 论文 T-ALNS-RRD | 数值含义 |
|
||||
|------|:--------------:|---------|
|
||||
| 总成本降低 | 24.3% vs Static | ~2157 vs ~2848 |
|
||||
| OTDR 提升 | 68.1% → 92.8% | 准时率大幅提升 |
|
||||
| CES 降低 | 54.4% | 拥堵暴露大幅减少 |
|
||||
| SOTA 改善 | 6.6% (p<0.001) | 相对元启发式 |
|
||||
|
||||
### 4.2 本次复现结果
|
||||
|
||||
| 指标 | 本次复现 | 论文 |
|
||||
|------|:-------:|:----:|
|
||||
| Static → TA 降幅 | **86.4%** | 未单独报告 TA |
|
||||
| TA → ALNS 降幅 | **5.7%** | 隐含在 24.3% 中 |
|
||||
| OTDR (最优) | 98.7% | 92.8% |
|
||||
| T-ALNS vs ALNS | ns (p=0.96) | — |
|
||||
|
||||
### 4.3 差异分析
|
||||
|
||||
| 差异项 | 原因 |
|
||||
|--------|------|
|
||||
| 复现 Static 成本更高 (9354 vs ~2848) | 论文未明确说明 Static 的评估方式。复现使用真实时变交通条件评估,delay 占比大。论文可能使用了不同的成本尺度或评估逻辑。 |
|
||||
| 复现 OTDR 更高 (98.7% vs 92.8%) | 交通感知贪心已经达到 95.6% OTDR,因为客户时间窗宽松(60-150 min),且完全图上有无限绕路可能。 |
|
||||
| T-ALNS 均值不显著 | SA 冷却不足 + 拥堵占成本主导 → Tabu 优化空间小 |
|
||||
|
||||
---
|
||||
|
||||
## 第五章:关键发现总结
|
||||
|
||||
### 5.1 核心结论
|
||||
|
||||
1. **交通感知是最大的单一贡献者** (成本 -86.4%, p<0.001):仅"知道何时何地拥堵"就消除了 99.9% 的迟到,降低了 38% 的拥堵暴露。在城配优化中,**信息优势远超算法优势**。
|
||||
|
||||
2. **ALNS 提供有意义的增量改进** (再降 5.7%, p<0.001):元启发式全局搜索在行程效率和残余迟到消除上产生显著改善,且降低了不同随机种子间解的方差。
|
||||
|
||||
3. **Tabu 记忆的效果受 SA 温度限制**:在 1000 次迭代和 γ=0.99975 的配置下,SA 仍处于高温探索状态,Tabu 的防循环效果被随机噪声掩盖。方差从 σ=3.57 降至 σ=3.01 表明稳定性在改善,但需要更多迭代才能显著体现。
|
||||
|
||||
4. **RRD 的事件驱动效果不显著**:在同步模拟架构下,事件触发频率低,rollout 模拟简化,dispatch 开销在无事件时为零。论文的双线程并行架构对 RRD 性能至关重要。
|
||||
|
||||
### 5.2 与前版本的差异
|
||||
|
||||
| 指标 | v2 calibrated | paper-fixed | 变化原因 |
|
||||
|------|:------------:|:-----------:|---------|
|
||||
| Static Cost | 15321.9 | 9354.3 | 行驶时间从路线总耗时改为纯弧遍历 |
|
||||
| TA Cost | 5013.1 | 1271.5 | 同上 + ρ=θ×γ 替代 ρ=θ×extra_time×γ |
|
||||
| ALNS Cost | 3246.8 | 1198.4 | 同上 |
|
||||
| Static Delay | 9017 | 7390 | travel time 拆分后,delay = route_end - travel - service |
|
||||
| ALNS σ | 104.2 | 3.6 | ρ 简化后成本地貌更平滑 |
|
||||
|
||||
### 5.3 局限性与改进方向
|
||||
|
||||
| 局限 | 建议 |
|
||||
|------|------|
|
||||
| SA 温度冷却太慢 (γ=0.99975) | 增加 max_iterations 至 5000+ 或调低 cooling_rate |
|
||||
| 拥堵惩罚 ρ 占成本主导 (~85%) | 可考虑降低 θ 或增加 λ₁ 以平衡成本成分 |
|
||||
| RRD 单线程模拟 | 实现真正的双线程并行架构 |
|
||||
| 缺少消融实验 | 补跑 Move Tabu / Freq Mem / Full 三级消融 |
|
||||
| 收敛曲线未绘制 | 利用已有 convergence.npz 数据生成收敛图 |
|
||||
|
||||
---
|
||||
|
||||
## 第六章:运行记录
|
||||
|
||||
### 实验命令
|
||||
```bash
|
||||
cd t_alns_rrd_reproduction
|
||||
pip install -r requirements.txt
|
||||
python src/experiments/run_main_comparison.py \
|
||||
--config paper --seeds 30 --iterations 1000 \
|
||||
--time-limit 600 --output results/paper_fixed
|
||||
```
|
||||
|
||||
### 实验产物
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `results/paper_fixed/tables/main_comparison.csv` | 主对比汇总表 |
|
||||
| `results/paper_fixed/tables/per_seed_costs.csv` | 30 seeds × 5 算法逐种子成本 |
|
||||
| `results/paper_fixed/tables/statistical_tests.csv` | Paired t-test 结果 |
|
||||
| `results/paper_fixed/logs/convergence.npz` | 每个算法的逐代最优成本轨迹 |
|
||||
| `results/paper_fixed/logs/run_main.log` | 完整运行 log |
|
||||
| `results/paper_fixed/figures/fig_main_comparison.png` | 四面板主对比图 |
|
||||
| `results/paper_fixed/figures/fig_significance.png` | 显著性热力图 |
|
||||
| `results/paper_fixed/config_used.yaml` | 使用的完整配置 |
|
||||
|
||||
---
|
||||
|
||||
## 复现声明
|
||||
|
||||
> 由于原文数据集需向作者合理请求,且目前尚未获得完整数据与代码,本项目采用与论文实验规模和数据结构相近的自定义合成数据集,复现其核心算法流程和对比实验框架。复现重点在于验证不同算法模块对总成本、准时率、拥堵暴露和实时扰动响应能力的相对影响,而非逐项复刻原文数值结果。本项目属于基于合成数据的算法机制复现 (methodological reproduction)。
|
||||
137
t_alns_rrd_reproduction/results/paper_fixed/config_used.yaml
Normal file
137
t_alns_rrd_reproduction/results/paper_fixed/config_used.yaml
Normal file
@@ -0,0 +1,137 @@
|
||||
# Canonical paper-aligned configuration for T-ALNS-RRD reproduction.
|
||||
# This is the primary server-run config. It preserves the paper's controlled
|
||||
# mid-scale instance: 47 customers, 4 homogeneous vehicles, 120 kg capacity,
|
||||
# 12 one-hour traffic intervals from 6:00 to 18:00, and 30 seeds.
|
||||
|
||||
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
|
||||
operating_end: 1080
|
||||
n_time_intervals: 12
|
||||
|
||||
customers:
|
||||
demand_min_kg: 3
|
||||
demand_max_kg: 12
|
||||
window_length_min: 60
|
||||
window_length_max: 150
|
||||
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"]
|
||||
|
||||
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:
|
||||
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
|
||||
risk_aversion_beta: 0.3
|
||||
uncertainty_base: 0.05
|
||||
|
||||
cost:
|
||||
lambda_lateness: 1.0
|
||||
lambda_congestion: 1.0
|
||||
lambda_stability: 0.3
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
rollout:
|
||||
horizon_min_min: 30
|
||||
horizon_max_min: 120
|
||||
urgency_alpha: 1.0
|
||||
n_sim_min: 2
|
||||
n_sim_max: 50
|
||||
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.5
|
||||
event_probability: 0.3
|
||||
event_check_interval: 10
|
||||
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
|
||||
|
||||
experiments:
|
||||
random_seeds: 30
|
||||
seed_start: 1
|
||||
report_mean_std: true
|
||||
statistical_testing: true
|
||||
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]
|
||||
58
t_alns_rrd_reproduction/results/paper_fixed/run_main.log
Normal file
58
t_alns_rrd_reproduction/results/paper_fixed/run_main.log
Normal file
@@ -0,0 +1,58 @@
|
||||
======================================================================
|
||||
T-ALNS-RRD Main Comparison [paper]
|
||||
Seeds: 1..30, Iter: 1000, Time: 600s
|
||||
======================================================================
|
||||
|
||||
[1/5] Generating dataset...
|
||||
Dataset generated: /home/huangfuqixun/workspace/enterprise/t_alns_rrd_reproduction/data/synthetic
|
||||
Customers: 47
|
||||
Arcs: 2256
|
||||
Traffic tensor: (48, 48, 12)
|
||||
Total data points: ~82944
|
||||
|
||||
[Static-VRPTW] Running 30 seeds...
|
||||
|
||||
0%| | 0/30 [00:00<?, ?it/s]
|
||||
3%|▎ | 1/30 [00:00<00:04, 6.32it/s]
|
||||
7%|▋ | 2/30 [00:00<00:04, 6.38it/s]
|
||||
10%|█ | 3/30 [00:00<00:04, 6.39it/s]
|
||||
13%|█▎ | 4/30 [00:00<00:04, 6.41it/s]
|
||||
17%|█▋ | 5/30 [00:00<00:03, 6.42it/s]
|
||||
20%|██ | 6/30 [00:00<00:03, 6.42it/s]
|
||||
23%|██▎ | 7/30 [00:01<00:03, 6.41it/s]
|
||||
27%|██▋ | 8/30 [00:01<00:03, 6.36it/s]
|
||||
30%|███ | 9/30 [00:01<00:03, 6.36it/s]
|
||||
33%|███▎ | 10/30 [00:01<00:03, 6.36it/s]
|
||||
37%|███▋ | 11/30 [00:01<00:02, 6.36it/s]
|
||||
40%|████ | 12/30 [00:01<00:02, 6.37it/s]
|
||||
43%|████▎ | 13/30 [00:02<00:02, 6.37it/s]
|
||||
47%|████▋ | 14/30 [00:02<00:02, 6.36it/s]
|
||||
50%|█████ | 15/30 [00:02<00:02, 6.39it/s]
|
||||
53%|█████▎ | 16/30 [00:02<00:02, 6.40it/s]
|
||||
57%|█████▋ | 17/30 [00:02<00:02, 6.39it/s]
|
||||
60%|██████ | 18/30 [00:02<00:01, 6.40it/s]
|
||||
63%|██████▎ | 19/30 [00:02<00:01, 6.40it/s]
|
||||
67%|██████▋ | 20/30 [00:03<00:01, 6.41it/s]
|
||||
70%|███████ | 21/30 [00:03<00:01, 6.38it/s]
|
||||
73%|███████▎ | 22/30 [00:03<00:01, 6.39it/s]
|
||||
77%|███████▋ | 23/30 [00:03<00:01, 6.39it/s]
|
||||
80%|████████ | 24/30 [00:03<00:00, 6.39it/s]
|
||||
83%|████████▎ | 25/30 [00:03<00:00, 6.39it/s]
|
||||
87%|████████▋ | 26/30 [00:04<00:00, 6.40it/s]
|
||||
90%|█████████ | 27/30 [00:04<00:00, 6.42it/s]
|
||||
93%|█████████▎| 28/30 [00:04<00:00, 6.43it/s]
|
||||
97%|█████████▋| 29/30 [00:04<00:00, 6.43it/s]
|
||||
100%|██████████| 30/30 [00:04<00:00, 6.43it/s]
|
||||
100%|██████████| 30/30 [00:04<00:00, 6.39it/s]
|
||||
Cost=9354.3±0.0 OTDR=57.4% CES=1769.6
|
||||
|
||||
[TA-VRPTW-Greedy] Running 30 seeds...
|
||||
|
||||
0%| | 0/30 [00:00<?, ?it/s]
|
||||
10%|█ | 3/30 [00:00<00:01, 22.37it/s]
|
||||
20%|██ | 6/30 [00:00<00:01, 20.37it/s]
|
||||
30%|███ | 9/30 [00:00<00:01, 19.35it/s]
|
||||
37%|███▋ | 11/30 [00:00<00:01, 18.79it/s]
|
||||
47%|████▋ | 14/30 [00:00<00:00, 19.57it/s]
|
||||
57%|█████▋ | 17/30 [00:00<00:00, 19.60it/s]
|
||||
63%|██████▎ | 19/30 [00:00<00:00, 19.68it/s]
|
||||
@@ -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,9354.336599999991,3.700170984052295e-12,57.446808510638306,0.0,1769.6376000000002,4.625213730065368e-13,194.53399999999993,7390.164999999998,1769.6376000000002,0.15597200393676758,369.50824999999963,531.4338999999997,20.0
|
||||
TA-VRPTW-Greedy,1271.5429266666665,21.898279178367986,95.60283687943264,2.675788190765346,1094.3976966666667,15.408475341803056,166.1685433333333,10.97668666666669,1094.3976966666667,0.04896566867828369,4.456140000000001,7.047916666666648,2.066666666666667
|
||||
ALNS-Base,1198.3858666666667,3.5668530942683696,98.51063829787233,1.3856568684743764,1077.81513,3.94942556810921,120.10344666666666,0.46729000000001025,1077.81513,31.414160950978598,0.4063183333333427,0.411800000000009,0.7
|
||||
T-ALNS,1198.4360433333334,3.0142268443892664,98.51063829787233,1.2680073048264633,1076.4449699999998,3.936876366390147,121.43683333333334,0.5542400000000005,1076.4449699999998,33.431352750460306,0.4482616666666691,0.508710000000004,0.7
|
||||
T-ALNS-RRD,1198.3182233333332,3.39367909702359,98.65248226950354,1.1830923761011403,1076.4059699999998,3.831026484318742,121.39075333333334,0.5215000000000032,1076.4059699999998,32.291462103525795,0.43189166666667045,0.4909266666666705,0.6333333333333333
|
||||
|
@@ -0,0 +1,11 @@
|
||||
algo_a,algo_b,t_statistic,p_value,significant
|
||||
Static-VRPTW,TA-VRPTW-Greedy,2021.6786837378515,3.2068141237198977e-76,***
|
||||
Static-VRPTW,ALNS-Base,12524.200118378203,3.4436402476561224e-99,***
|
||||
Static-VRPTW,T-ALNS,14820.287066218702,2.6117174361391014e-101,***
|
||||
Static-VRPTW,T-ALNS-RRD,13163.399121162758,8.130072129529089e-100,***
|
||||
TA-VRPTW-Greedy,ALNS-Base,18.5536525614471,1.2426941015708843e-17,***
|
||||
TA-VRPTW-Greedy,T-ALNS,18.38040998746316,1.5979158169693442e-17,***
|
||||
TA-VRPTW-Greedy,T-ALNS-RRD,18.614318488614693,1.1384998643238919e-17,***
|
||||
ALNS-Base,T-ALNS,-0.05072081401824748,0.9598957400618513,ns
|
||||
ALNS-Base,T-ALNS-RRD,0.06581044035553706,0.9479803419104813,ns
|
||||
T-ALNS,T-ALNS-RRD,0.45800454612205393,0.6503626551192184,ns
|
||||
|
@@ -207,6 +207,7 @@ class ALNSBase:
|
||||
new_cost = self.cost_calc.compute_total_cost(S_new, self.ctx)
|
||||
|
||||
# Acceptance decision (Eq.20)
|
||||
previous_cost = current_cost
|
||||
if sa.accept(current_cost, new_cost):
|
||||
S_current = S_new
|
||||
current_cost = new_cost
|
||||
@@ -216,9 +217,12 @@ class ALNSBase:
|
||||
best_cost = new_cost
|
||||
stall_counter = 0
|
||||
reward = self.cfg["reward_global_best"]
|
||||
else:
|
||||
elif new_cost < previous_cost:
|
||||
stall_counter += 1
|
||||
reward = self.cfg["reward_improvement"]
|
||||
else:
|
||||
stall_counter += 1
|
||||
reward = self.cfg["reward_accepted"]
|
||||
else:
|
||||
reward = self.cfg["reward_rejected"]
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ class CostCalculator:
|
||||
departures = [0.0] * n
|
||||
delays = [0.0] * n
|
||||
waits = [0.0] * n
|
||||
arc_travel_time = 0.0
|
||||
congestion_exposure = 0.0
|
||||
|
||||
# First node (depot)
|
||||
@@ -61,10 +62,12 @@ class CostCalculator:
|
||||
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]
|
||||
)
|
||||
# Arrival at j (Eq.5). Eq.1's travel term is the sum of arc
|
||||
# traversal times only; waiting and service duration are temporal
|
||||
# propagation state, not travel cost.
|
||||
travel_time = problem_ctx.get_travel_time(i, j, departures[idx - 1])
|
||||
arc_travel_time += travel_time
|
||||
arrivals[idx] = departures[idx - 1] + travel_time
|
||||
|
||||
# Accumulate congestion exposure
|
||||
congestion_exposure += problem_ctx.get_congestion_penalty(
|
||||
@@ -103,7 +106,8 @@ class CostCalculator:
|
||||
"departures": departures,
|
||||
"delays": delays,
|
||||
"waits": waits,
|
||||
"total_travel_time": arrivals[-1] - depot_start_time,
|
||||
"total_travel_time": arc_travel_time,
|
||||
"route_duration": departures[-1] - depot_start_time,
|
||||
"total_delay": sum(delays),
|
||||
"total_wait": sum(waits),
|
||||
"congestion_exposure": congestion_exposure,
|
||||
@@ -381,9 +385,7 @@ class CostCalculator:
|
||||
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]
|
||||
)
|
||||
avg_route_duration += result["route_duration"]
|
||||
|
||||
# Count on-time deliveries
|
||||
for idx, node in enumerate(route.nodes):
|
||||
|
||||
@@ -324,10 +324,8 @@ class DataGenerator:
|
||||
# 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
|
||||
# Congestion penalty ρ (Eq.9): θ × normalized density γ.
|
||||
rho = self.congestion_scale * gamma
|
||||
|
||||
travel_time[i, j, h] = round(tt, 4)
|
||||
congestion[i, j, h] = round(gamma, 4)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Main comparison experiment (v2).
|
||||
Main comparison experiment.
|
||||
|
||||
Runs all 5 algorithms with configurable settings.
|
||||
Supports --config flag for version switching.
|
||||
@@ -35,8 +35,78 @@ except ImportError:
|
||||
HAS_SCIPY = False
|
||||
|
||||
|
||||
def build_algorithm_config(cfg: dict, max_iterations: int, time_limit_sec: int) -> dict:
|
||||
"""Flatten YAML sections into solver constructor config keys."""
|
||||
alns = cfg.get("alns", {})
|
||||
tabu = cfg.get("tabu", {})
|
||||
rrd = cfg.get("rrd", {})
|
||||
rollout = rrd.get("rollout", {})
|
||||
dispatch = rrd.get("dispatch", {})
|
||||
rrd_tabu = rrd.get("tabu", {})
|
||||
events = rrd.get("events", {})
|
||||
|
||||
return {
|
||||
"max_iterations": max_iterations,
|
||||
"time_limit_sec": time_limit_sec,
|
||||
"destroy_ratio_min": alns.get("destroy_ratio_min", 0.1),
|
||||
"destroy_ratio_max": alns.get("destroy_ratio_max", 0.4),
|
||||
"initial_temperature_factor": alns.get("initial_temperature_factor", 0.05),
|
||||
"cooling_rate": alns.get("cooling_rate", 0.99975),
|
||||
"reaction_factor": alns.get("reaction_factor", 0.1),
|
||||
"segment_length": alns.get("segment_length", 100),
|
||||
"stall_limit": alns.get("stall_limit", 200),
|
||||
"max_attempts": alns.get("max_attempts", 5),
|
||||
"reward_global_best": alns.get("reward_global_best", 1.0),
|
||||
"reward_improvement": alns.get("reward_improvement", 0.5),
|
||||
"reward_accepted": alns.get("reward_accepted", 0.2),
|
||||
"reward_rejected": alns.get("reward_rejected", 0.0),
|
||||
"move_tabu_tenure": tabu.get("move_tabu", {}).get("tenure", 7),
|
||||
"move_tabu_tenure_min": tabu.get("move_tabu", {}).get("tenure_min", 3),
|
||||
"move_tabu_tenure_max": tabu.get("move_tabu", {}).get("tenure_max", 12),
|
||||
"move_tabu_overlap_threshold": tabu.get("move_tabu", {}).get(
|
||||
"overlap_threshold", 0.5
|
||||
),
|
||||
"move_tabu_stall_for_increase": tabu.get("move_tabu", {}).get(
|
||||
"stall_for_increase", 50
|
||||
),
|
||||
"solution_tabu_tenure": tabu.get("solution_tabu", {}).get("tenure", 15),
|
||||
"solution_tabu_buffer": tabu.get("solution_tabu", {}).get("buffer_size", 1000),
|
||||
"solution_tabu_prime": tabu.get("solution_tabu", {}).get(
|
||||
"hash_prime", 1000000007
|
||||
),
|
||||
"freq_norm_factor": tabu.get("frequency", {}).get("normalization_factor", 2.0),
|
||||
"freq_norm_interval": tabu.get("frequency", {}).get(
|
||||
"normalization_interval", 50
|
||||
),
|
||||
"diversification_delta_max": tabu.get("diversification", {}).get(
|
||||
"delta_max", 0.7
|
||||
),
|
||||
"diversification_eta": tabu.get("diversification", {}).get(
|
||||
"eta_balance", 0.5
|
||||
),
|
||||
"diversification_weights": tabu.get("diversification", {}).get(
|
||||
"weights", [0.4, 0.3, 0.3]
|
||||
),
|
||||
"aspiration_beta": tabu.get("aspiration", {}).get("beta_threshold", 0.3),
|
||||
"aspiration_gamma": tabu.get("aspiration", {}).get("gamma_threshold", 0.8),
|
||||
"rollout_horizon_min": rollout.get("horizon_min_min", 30),
|
||||
"rollout_horizon_max": rollout.get("horizon_max_min", 120),
|
||||
"rollout_n_sim_min": rollout.get("n_sim_min", 2),
|
||||
"rollout_n_sim_max": rollout.get("n_sim_max", 50),
|
||||
"rollout_mc_iterations": rollout.get("mc_iterations", 50),
|
||||
"dispatch_weight_rollout": dispatch.get("weight_rollout", 0.4),
|
||||
"dispatch_weight_stability": dispatch.get("weight_stability", 0.3),
|
||||
"dispatch_weight_recovery": dispatch.get("weight_recovery", 0.3),
|
||||
"event_urgency_threshold": events.get("urgency_threshold", 0.5),
|
||||
"event_probability": events.get("event_probability", 0.3),
|
||||
"event_check_interval": events.get("event_check_interval", 10),
|
||||
"rrd_tabu_penalty": rrd_tabu.get("penalty", 50.0),
|
||||
"rrd_tabu_bonus": rrd_tabu.get("bonus", 25.0),
|
||||
}
|
||||
|
||||
|
||||
def run_experiment(
|
||||
config_name="calibrated",
|
||||
config_name="paper",
|
||||
n_seeds=None,
|
||||
max_iterations=None,
|
||||
time_limit_sec=None,
|
||||
@@ -66,10 +136,14 @@ def run_experiment(
|
||||
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)
|
||||
seed_start = cfg.get("experiments", {}).get("seed_start", 0)
|
||||
|
||||
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(
|
||||
f"Seeds: {seed_start}..{seed_start + n_seeds - 1}, "
|
||||
f"Iter: {max_iterations}, Time: {time_limit_sec}s"
|
||||
)
|
||||
print("=" * 70)
|
||||
|
||||
print("\n[1/5] Generating dataset...")
|
||||
@@ -80,6 +154,9 @@ def run_experiment(
|
||||
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"],
|
||||
op_start=cfg["problem"]["operating_start"],
|
||||
op_end=cfg["problem"]["operating_end"],
|
||||
n_intervals=cfg["problem"]["n_time_intervals"],
|
||||
)
|
||||
cost_calc = CostCalculator(
|
||||
lambda_lateness=cfg["cost"]["lambda_lateness"],
|
||||
@@ -87,15 +164,7 @@ def run_experiment(
|
||||
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,
|
||||
}
|
||||
alg_cfg = build_algorithm_config(cfg, max_iterations, time_limit_sec)
|
||||
|
||||
algorithms = [
|
||||
("Static-VRPTW", StaticVRPTWSolver, {}),
|
||||
@@ -114,7 +183,7 @@ def run_experiment(
|
||||
alg_results = []
|
||||
seed_costs = []
|
||||
|
||||
for seed in tqdm(range(n_seeds)):
|
||||
for seed in tqdm(range(seed_start, seed_start + n_seeds)):
|
||||
if alg_name in ("Static-VRPTW", "TA-VRPTW-Greedy"):
|
||||
solver = solver_cls(ctx, cost_calc)
|
||||
else:
|
||||
@@ -190,7 +259,7 @@ def run_experiment(
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--config", default="calibrated")
|
||||
parser.add_argument("--config", default="paper")
|
||||
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)
|
||||
|
||||
@@ -147,7 +147,12 @@ class ProblemContext:
|
||||
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]."""
|
||||
"""Map time to interval h.
|
||||
|
||||
The paper discretizes traffic feeds for 6:00-18:00. Customer windows may
|
||||
extend to 20:00, so departures after the final traffic bucket use
|
||||
hold-last-value semantics instead of extrapolating unobserved traffic.
|
||||
"""
|
||||
minutes = max(self.op_start, min(minutes, self.op_end - 1))
|
||||
return int((minutes - self.op_start) / self.interval_duration)
|
||||
|
||||
|
||||
@@ -88,26 +88,16 @@ def generate_actions_E2_urgent(
|
||||
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):
|
||||
# The fixed 47-node benchmark has no live road matrix for new customer
|
||||
# coordinates. Keep E2 feasible by representing immediate handling as
|
||||
# subcontracting or customer notification penalties instead of inserting
|
||||
# invalid synthetic node IDs into the fixed tensor.
|
||||
delay = max(0.0, deadline - event.time_min)
|
||||
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}",
|
||||
"type": "urgent_defer",
|
||||
"penalty_cost": 120.0 + max(0.0, 60.0 - delay),
|
||||
"description": "Delay urgent delivery with customer notification",
|
||||
})
|
||||
break # Just try one position per vehicle for speed
|
||||
if len(actions) >= 10:
|
||||
break
|
||||
|
||||
# Subcontract option (penalty-based)
|
||||
actions.append({
|
||||
@@ -204,6 +194,7 @@ def generate_actions_E4_timewindow(
|
||||
"type": "temporary_tolerance",
|
||||
"customer": affected,
|
||||
"tolerance_min": 30.0,
|
||||
"penalty_cost": 30.0,
|
||||
"description": f"Grant 30min tolerance for customer {affected}",
|
||||
})
|
||||
|
||||
|
||||
@@ -37,10 +37,16 @@ class Dispatch:
|
||||
self.w_rollout = weight_rollout # ω₁
|
||||
self.w_stability = weight_stability # ω₂
|
||||
self.w_recovery = weight_recovery # ω₃
|
||||
self._seed = seed
|
||||
self.rng = np.random.default_rng(seed)
|
||||
|
||||
self.dispatch_log: List[dict] = []
|
||||
|
||||
def reset(self, seed: int = None):
|
||||
"""Reset dispatch RNG and per-run log for deterministic experiments."""
|
||||
self.rng = np.random.default_rng(self._seed if seed is None else seed)
|
||||
self.dispatch_log = []
|
||||
|
||||
def compute_stability(self, action: dict, current_solution: Solution) -> float:
|
||||
"""Compute route stability score (Eq.41).
|
||||
|
||||
@@ -186,6 +192,7 @@ class Dispatch:
|
||||
"composite_score": best["composite_score"],
|
||||
"response_time_ms": elapsed_ms,
|
||||
"n_actions_evaluated": len(actions),
|
||||
"description": best["action"].get("description", ""),
|
||||
}
|
||||
self.dispatch_log.append(log_entry)
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ class EventGenerator:
|
||||
cost_calc: "CostCalculator",
|
||||
urgency_threshold: float = 0.5,
|
||||
event_weights: dict = None,
|
||||
seed: int = None,
|
||||
):
|
||||
self.ctx = problem_ctx
|
||||
self.cost_calc = cost_calc
|
||||
@@ -57,7 +58,8 @@ class EventGenerator:
|
||||
EventType.E4_TIMEWINDOW: (0.8, 0.1, 0.1),
|
||||
}
|
||||
|
||||
self.rng = np.random.default_rng(None)
|
||||
self._seed = seed
|
||||
self.rng = np.random.default_rng(seed)
|
||||
self.event_log: List[Event] = []
|
||||
|
||||
def generate_traffic_incident(
|
||||
@@ -116,16 +118,14 @@ class EventGenerator:
|
||||
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
|
||||
# Store incident severity for rollout evaluation. The live traffic tensor
|
||||
# is modified only while a selected dispatch action is being evaluated,
|
||||
# then restored by TALNSRRD.
|
||||
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
|
||||
event._severity = severity
|
||||
|
||||
return event
|
||||
|
||||
@@ -310,7 +310,7 @@ class EventGenerator:
|
||||
self.event_log.extend(events)
|
||||
return events
|
||||
|
||||
def reset(self):
|
||||
def reset(self, seed: int = None):
|
||||
"""Reset event log for a new run."""
|
||||
self.event_log = []
|
||||
self.rng = np.random.default_rng(None)
|
||||
self.rng = np.random.default_rng(self._seed if seed is None else seed)
|
||||
|
||||
@@ -8,7 +8,6 @@ Rollout horizon H: 30-120 minutes
|
||||
Monte Carlo iterations: 2-50
|
||||
"""
|
||||
|
||||
import time
|
||||
import numpy as np
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
@@ -45,8 +44,13 @@ class RolloutEngine:
|
||||
self.mc_iterations = mc_iterations
|
||||
self.tabu_penalty = tabu_penalty
|
||||
self.tabu_bonus = tabu_bonus
|
||||
self._seed = seed
|
||||
self.rng = np.random.default_rng(seed)
|
||||
|
||||
def reset(self, seed: int = None):
|
||||
"""Reset stochastic rollout sampling for deterministic seeded runs."""
|
||||
self.rng = np.random.default_rng(self._seed if seed is None else seed)
|
||||
|
||||
def adapt_horizon(self, urgency: float) -> int:
|
||||
"""Adapt rollout horizon based on event urgency (Eq.43).
|
||||
|
||||
@@ -101,37 +105,60 @@ class RolloutEngine:
|
||||
|
||||
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
|
||||
total_cost = float(action.get("penalty_cost", 0.0))
|
||||
|
||||
# Simple forward simulation: evaluate each route with noise
|
||||
for route in sim_sol.routes:
|
||||
if len(route.nodes) < 2:
|
||||
continue
|
||||
current_time = self.ctx.op_start
|
||||
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
|
||||
# Add noise to travel time. Local reroute is a road-level
|
||||
# detour in the complete-graph reproduction, so the route's
|
||||
# customer sequence stays fixed while this arc is costed via
|
||||
# an intermediate road waypoint.
|
||||
if (
|
||||
action.get("type") == "local_reroute"
|
||||
and tuple(action.get("arc", ())) == (i, j)
|
||||
and 0 < action.get("bypass", -1) < self.ctx.n_nodes
|
||||
):
|
||||
bypass = action["bypass"]
|
||||
base_tt = (
|
||||
self.ctx.get_travel_time(i, bypass, current_time)
|
||||
+ self.ctx.get_travel_time(bypass, j, current_time)
|
||||
)
|
||||
congestion = (
|
||||
self.ctx.get_congestion_penalty(i, bypass, current_time)
|
||||
+ self.ctx.get_congestion_penalty(bypass, j, current_time)
|
||||
)
|
||||
else:
|
||||
base_tt = self.ctx.get_travel_time(i, j, current_time)
|
||||
congestion = self.ctx.get_congestion_penalty(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)
|
||||
tolerance = (
|
||||
action.get("tolerance_min", 0.0)
|
||||
if action.get("type") == "temporary_tolerance"
|
||||
and action.get("customer") == j
|
||||
else 0.0
|
||||
)
|
||||
lateness = max(0.0, current_time - cust.latest_time_min - tolerance)
|
||||
total_cost += self.cost_calc.lambda_lateness * lateness
|
||||
|
||||
# Service time
|
||||
@@ -182,22 +209,21 @@ class RolloutEngine:
|
||||
if removed:
|
||||
is_tabu = tabu_mem.is_tabu(removed, action["type"], "", 0)
|
||||
if is_tabu:
|
||||
V_adjusted -= self.tabu_penalty
|
||||
V_adjusted += self.tabu_penalty
|
||||
else:
|
||||
V_adjusted += self.tabu_bonus
|
||||
V_adjusted = max(0.0, 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
|
||||
# Complete-graph reproduction: route sequence is unchanged and the
|
||||
# detour effect is represented by action penalty/rollout scoring.
|
||||
return sol
|
||||
|
||||
elif action_type == "customer_reassign":
|
||||
cust = action.get("customer")
|
||||
@@ -213,16 +239,13 @@ class RolloutEngine:
|
||||
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)
|
||||
# Urgent customers are not part of the fixed 47-customer tensor in
|
||||
# this methodological reproduction. They are scored via penalty
|
||||
# actions; inserting synthetic negative nodes would invalidate Eq.1.
|
||||
return sol
|
||||
|
||||
elif action_type == "subcontract":
|
||||
# No route change, just penalty
|
||||
pass
|
||||
elif action_type in ("urgent_defer", "subcontract"):
|
||||
return sol
|
||||
|
||||
elif action_type == "redistribute":
|
||||
cust = action.get("customer")
|
||||
@@ -249,7 +272,6 @@ class RolloutEngine:
|
||||
pass
|
||||
|
||||
elif action_type == "temporary_tolerance":
|
||||
# Accept delay without route change
|
||||
pass
|
||||
return sol
|
||||
|
||||
return sol
|
||||
|
||||
@@ -62,8 +62,12 @@ class TALNSRRD:
|
||||
"dispatch_weight_rollout": 0.4,
|
||||
"dispatch_weight_stability": 0.3,
|
||||
"dispatch_weight_recovery": 0.3,
|
||||
"event_urgency_threshold": 0.5,
|
||||
"event_probability": 0.3,
|
||||
"event_check_interval": 10, # Check for events every N iterations
|
||||
"rollout_mc_iterations": 50,
|
||||
"rrd_tabu_penalty": 50.0,
|
||||
"rrd_tabu_bonus": 25.0,
|
||||
}
|
||||
if config:
|
||||
default_cfg.update(config)
|
||||
@@ -75,7 +79,7 @@ class TALNSRRD:
|
||||
# Initialize RRD components
|
||||
self.event_generator = EventGenerator(
|
||||
problem_ctx, cost_calc,
|
||||
urgency_threshold=0.3, # Lower threshold for more events
|
||||
urgency_threshold=self.cfg["event_urgency_threshold"],
|
||||
)
|
||||
|
||||
self.rollout_engine = RolloutEngine(
|
||||
@@ -84,6 +88,9 @@ class TALNSRRD:
|
||||
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"],
|
||||
mc_iterations=self.cfg["rollout_mc_iterations"],
|
||||
tabu_penalty=self.cfg["rrd_tabu_penalty"],
|
||||
tabu_bonus=self.cfg["rrd_tabu_bonus"],
|
||||
)
|
||||
|
||||
self.dispatch = Dispatch(
|
||||
@@ -99,6 +106,34 @@ class TALNSRRD:
|
||||
self.event_count = 0
|
||||
self.dispatched_count = 0
|
||||
|
||||
@staticmethod
|
||||
def _seed_with_offset(seed: int, offset: int):
|
||||
return None if seed is None else seed + offset
|
||||
|
||||
def _apply_event_traffic(self, event) -> dict:
|
||||
"""Temporarily apply incident severity to the affected traffic arc."""
|
||||
modified_arc = getattr(event, "_modified_arc", None)
|
||||
severity = getattr(event, "_severity", None)
|
||||
if modified_arc is None or severity is None:
|
||||
return {}
|
||||
|
||||
i, j = modified_arc
|
||||
backup = {
|
||||
"arc": (i, j),
|
||||
"travel_time": self.ctx.traffic.travel_time[i, j, :].copy(),
|
||||
"congestion_penalty": self.ctx.traffic.congestion_penalty[i, j, :].copy(),
|
||||
}
|
||||
self.ctx.traffic.travel_time[i, j, :] *= severity
|
||||
self.ctx.traffic.congestion_penalty[i, j, :] *= severity
|
||||
return backup
|
||||
|
||||
def _restore_event_traffic(self, backup: dict):
|
||||
if not backup:
|
||||
return
|
||||
i, j = backup["arc"]
|
||||
self.ctx.traffic.travel_time[i, j, :] = backup["travel_time"]
|
||||
self.ctx.traffic.congestion_penalty[i, j, :] = backup["congestion_penalty"]
|
||||
|
||||
def solve(self, seed: int = None) -> Solution:
|
||||
"""Run T-ALNS-RRD optimization (Algorithm 3)."""
|
||||
rng = np.random.default_rng(seed)
|
||||
@@ -108,7 +143,11 @@ class TALNSRRD:
|
||||
self.talns.move_tabu.clear()
|
||||
self.talns.sol_tabu.clear()
|
||||
self.talns.freq_mem.clear()
|
||||
self.event_generator.reset()
|
||||
self.event_generator.reset(self._seed_with_offset(seed, 101))
|
||||
self.rollout_engine.reset(self._seed_with_offset(seed, 202))
|
||||
self.dispatch.reset(self._seed_with_offset(seed, 303))
|
||||
self.event_count = 0
|
||||
self.dispatched_count = 0
|
||||
|
||||
# Initialize weights
|
||||
self.talns.destroy_weights = {name: 1.0 for name in self.talns.destroy_ops}
|
||||
@@ -162,30 +201,29 @@ class TALNSRRD:
|
||||
"move_tabu": self.talns.move_tabu,
|
||||
}
|
||||
|
||||
# Select and apply dispatch action
|
||||
backup = self._apply_event_traffic(event)
|
||||
action = None
|
||||
S_dispatched = None
|
||||
try:
|
||||
# Select and apply dispatch action under the temporary
|
||||
# disrupted traffic state, then restore the base tensor.
|
||||
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)
|
||||
S_dispatched = self.dispatch.apply_action(action, S_current)
|
||||
finally:
|
||||
self._restore_event_traffic(backup)
|
||||
|
||||
if action is not None and S_dispatched is not None:
|
||||
S_current = S_dispatched
|
||||
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)
|
||||
self.talns._update_frequency_memory(S_current)
|
||||
|
||||
if current_cost < best_cost:
|
||||
S_best = S_current.copy()
|
||||
@@ -239,19 +277,22 @@ class TALNSRRD:
|
||||
# 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):
|
||||
if not self.talns._check_aspiration(
|
||||
S_new, new_cost_temp, best_cost, removed, S_current
|
||||
):
|
||||
continue
|
||||
|
||||
new_cost = self.cost_calc.compute_total_cost(S_new, self.ctx)
|
||||
|
||||
# Acceptance
|
||||
previous_cost = current_cost
|
||||
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)
|
||||
self.talns._update_frequency_memory(S_new)
|
||||
|
||||
if new_cost < best_cost:
|
||||
S_best = S_new.copy()
|
||||
@@ -260,10 +301,14 @@ class TALNSRRD:
|
||||
last_best_iter = iter_count
|
||||
self.talns.move_tabu.update_tenure(found_improvement=True)
|
||||
reward = self.cfg["reward_global_best"]
|
||||
elif new_cost < previous_cost:
|
||||
stall_counter += 1
|
||||
self.talns.move_tabu.update_tenure(found_improvement=True)
|
||||
reward = self.cfg["reward_improvement"]
|
||||
else:
|
||||
stall_counter += 1
|
||||
self.talns.move_tabu.update_tenure(found_improvement=False)
|
||||
reward = self.cfg["reward_improvement"]
|
||||
reward = self.cfg["reward_accepted"]
|
||||
accepted = True
|
||||
else:
|
||||
reward = self.cfg["reward_rejected"]
|
||||
|
||||
@@ -75,6 +75,11 @@ class MoveTabu:
|
||||
while len(self._entries) > self.tenure_max:
|
||||
self._entries.popleft()
|
||||
|
||||
@property
|
||||
def utilization(self) -> float:
|
||||
"""Normalized |T_move| / |T_move|max for Eq.27 diversification."""
|
||||
return len(self._entries) / max(self.tenure_max, 1)
|
||||
|
||||
def update_tenure(self, found_improvement: bool):
|
||||
"""Adapt tenure based on improvement (Eq.32).
|
||||
|
||||
|
||||
@@ -160,8 +160,9 @@ class TALNS:
|
||||
# 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"]
|
||||
# Move Tabu utilization: Eq.27 uses actual memory occupancy |T_move|,
|
||||
# not the adaptive tenure parameter from Eq.32.
|
||||
move_factor = self.move_tabu.utilization
|
||||
|
||||
# Frequency std
|
||||
freq_std = self.freq_mem.get_std_assignment_freq()
|
||||
@@ -223,6 +224,7 @@ class TALNS:
|
||||
new_cost: float,
|
||||
best_cost: float,
|
||||
removed_customers: List[int],
|
||||
S_current: Solution = None,
|
||||
) -> bool:
|
||||
"""Check aspiration criteria (Eq.29-31).
|
||||
|
||||
@@ -239,14 +241,38 @@ class TALNS:
|
||||
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
|
||||
# Traffic adaptation aspiration (Eq.31): accept if the candidate
|
||||
# significantly reduces congestion exposure versus the current route set.
|
||||
if S_current is not None:
|
||||
new_congestion = self.cost_calc.compute_congestion_exposure(S_new, self.ctx)
|
||||
current_congestion = self.cost_calc.compute_congestion_exposure(
|
||||
S_current, self.ctx
|
||||
)
|
||||
if new_congestion < self.cfg["aspiration_gamma"] * current_congestion:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _route_congestion_weights(self, solution: Solution) -> Dict[tuple, float]:
|
||||
"""Build incoming-arc congestion weights for Eq.34 frequency memory."""
|
||||
weights = {}
|
||||
for route in solution.routes:
|
||||
result = self.cost_calc.propagate_route(route.nodes, self.ctx)
|
||||
for idx in range(1, len(route.nodes)):
|
||||
i = route.nodes[idx - 1]
|
||||
j = route.nodes[idx]
|
||||
if j == 0:
|
||||
continue
|
||||
depart = result["departures"][idx - 1]
|
||||
weights[(i, j)] = self.ctx.get_congestion_penalty(i, j, depart)
|
||||
return weights
|
||||
|
||||
def _update_frequency_memory(self, solution: Solution):
|
||||
self.freq_mem.update(
|
||||
solution,
|
||||
congestion_weights=self._route_congestion_weights(solution),
|
||||
)
|
||||
|
||||
def solve(self, seed: int = None) -> Solution:
|
||||
"""Run T-ALNS optimization (Algorithm 2)."""
|
||||
rng = np.random.default_rng(seed)
|
||||
@@ -345,13 +371,16 @@ class TALNS:
|
||||
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):
|
||||
if not self._check_aspiration(
|
||||
S_new, new_cost_temp, best_cost, removed, S_current
|
||||
):
|
||||
continue
|
||||
|
||||
# Evaluate cost
|
||||
new_cost = self.cost_calc.compute_total_cost(S_new, self.ctx)
|
||||
|
||||
# Acceptance decision
|
||||
previous_cost = current_cost
|
||||
if sa.accept(current_cost, new_cost):
|
||||
S_current = S_new
|
||||
current_cost = new_cost
|
||||
@@ -362,7 +391,7 @@ class TALNS:
|
||||
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)
|
||||
self._update_frequency_memory(S_new)
|
||||
|
||||
if new_cost < best_cost:
|
||||
S_best = S_new.copy()
|
||||
@@ -371,10 +400,14 @@ class TALNS:
|
||||
last_best_iter = iter_count
|
||||
self.move_tabu.update_tenure(found_improvement=True)
|
||||
reward = self.cfg["reward_global_best"]
|
||||
elif new_cost < previous_cost:
|
||||
stall_counter += 1
|
||||
self.move_tabu.update_tenure(found_improvement=True)
|
||||
reward = self.cfg["reward_improvement"]
|
||||
else:
|
||||
stall_counter += 1
|
||||
self.move_tabu.update_tenure(found_improvement=False)
|
||||
reward = self.cfg["reward_improvement"]
|
||||
reward = self.cfg["reward_accepted"]
|
||||
accepted = True
|
||||
else:
|
||||
reward = self.cfg["reward_rejected"]
|
||||
|
||||
Reference in New Issue
Block a user