feat(javaTest): plan traffic-aware approach and service routes

This commit is contained in:
zgw
2026-08-06 17:29:00 +08:00
parent 5719fe4a5f
commit 87129e39b4
4 changed files with 211 additions and 23 deletions

View File

@@ -6,6 +6,12 @@ import com.ft546.wcs.demo.model.DispatchTask;
import com.ft546.wcs.demo.model.Site;
import com.ft546.wcs.demo.model.VehicleSnapshot;
import com.ft546.wcs.demo.traffic.GridPathPlanner;
import com.ft546.wcs.demo.traffic.RouteCandidatePlanner;
import com.ft546.wcs.demo.traffic.RouteCandidateSet;
import com.ft546.wcs.demo.traffic.RoutePlanDecision;
import com.ft546.wcs.demo.traffic.RoutePlanningPolicy;
import com.ft546.wcs.demo.traffic.RouteQualityEvaluator;
import com.ft546.wcs.demo.traffic.TrafficRouteSnapshot;
import com.ft546.wcs.shadow.application.ShadowTrafficCoordinator;
import com.ft546.wcs.shadow.decision.DecisionType;
import com.ft546.wcs.shadow.decision.RollingDispatchPolicy;
@@ -77,7 +83,8 @@ public final class HistoricalDispatchSimulator {
SimulationTaskStatus.FAILED, failure.getReason(), null,
Collections.emptyList(), Collections.emptyList(), 0L,
null, null, 0, 0, 0,
Collections.emptyMap(), Collections.emptyMap()));
Collections.emptyMap(), Collections.emptyMap(),
Collections.emptyList()));
}
Map<String, SimulatedVehicleRuntime> vehicles = createRuntimes(input);
@@ -91,6 +98,8 @@ public final class HistoricalDispatchSimulator {
SimulationArchive archive = new SimulationArchive();
IdempotentWmsCompletionReceiver wms = new IdempotentWmsCompletionReceiver();
GridPathPlanner planner = new GridPathPlanner();
RouteCandidatePlanner routePlanner = new RouteCandidatePlanner();
RouteQualityEvaluator routeEvaluator = new RouteQualityEvaluator();
VehicleAssignmentService assignment = new VehicleAssignmentService();
CorridorReservationManager reservations = new CorridorReservationManager();
ClearanceMovePlanner clearancePlanner = new ClearanceMovePlanner();
@@ -105,8 +114,8 @@ public final class HistoricalDispatchSimulator {
applyVehicleEvents(tick, now, faults, vehicles, active,
clearanceMoves, queue, executions, reservations, events);
assignReadyTasks(tick, now, input, queue, vehicles, active,
clearanceMoves, executions, terminal, planner, assignment, plc,
reservations, events);
clearanceMoves, executions, terminal, routePlanner, routeEvaluator,
assignment, plc, ledger, reservations, events);
// 本 tick 已通过的边被共享给两类移动,禁止另一车辆同 tick 重复或反向穿越。
Set<String> traversedEdges = new LinkedHashSet<String>();
ClearanceProgress clearanceProgress = advanceClearanceMoves(
@@ -269,9 +278,11 @@ public final class HistoricalDispatchSimulator {
* @param clearanceMoves 执行该处理所需的 clearance moves 输入。
* @param executions 执行该处理所需的 executions 输入。
* @param terminal 执行该处理所需的 terminal 输入。
* @param planner 执行该处理所需的 planner 输入。
* @param routePlanner 生成 110% 物理距离内的确定性无环候选。
* @param routeEvaluator 使用同一交通快照按固定业务元组选择候选。
* @param assignment 执行该处理所需的 assignment 输入。
* @param plc 执行该处理所需的 plc 输入。
* @param ledger 当前节点和边资源的权威台账。
* @param reservations 执行该处理所需的 reservations 输入。
* @param events 执行该处理所需的 events 输入。
* @return 本 tick 是否至少有一个任务被分配或因永久不可达关闭。
@@ -283,9 +294,11 @@ public final class HistoricalDispatchSimulator {
Map<String, ClearanceMove> clearanceMoves,
Map<String, Execution> executions,
Map<String, SimulationTaskResult> terminal,
GridPathPlanner planner,
RouteCandidatePlanner routePlanner,
RouteQualityEvaluator routeEvaluator,
VehicleAssignmentService assignment,
SimulatedPlcGateway plc,
ResourceLedger ledger,
CorridorReservationManager reservations,
List<SimulationTickEvent> events) {
boolean progress = false;
@@ -295,10 +308,11 @@ public final class HistoricalDispatchSimulator {
for (HistoricalSimulationTask task : ready) {
if (terminal.containsKey(task.getSimulationTaskId())) continue;
// 服务路径由完整地图重新计算。历史 path_info 不参与规划,空路径表示业务端点永久不可达。
List<String> servicePath = planner.shortestPath(input.getGridMap(),
task.getPhysicalSource(), task.getPhysicalTarget(), Collections.emptySet());
if (servicePath.isEmpty()) {
// 先生成服务段物理候选。这里只使用永久地图,临时占用不能把任务误关为 NO_ROUTE。
RouteCandidateSet serviceCandidates = routePlanner.plan(input.getGridMap(),
task.getPhysicalSource(), task.getPhysicalTarget(),
Collections.<String>emptySet(), RoutePlanningPolicy.defaultPolicy());
if (serviceCandidates.getCandidates().isEmpty()) {
queue.remove(task.getSimulationTaskId());
terminal.put(task.getSimulationTaskId(), result(executions.get(
task.getSimulationTaskId()), SimulationTaskStatus.FAILED,
@@ -308,22 +322,24 @@ public final class HistoricalDispatchSimulator {
// 为每台空闲且健康的车计算到任务源点的接近路径,无法接近源点的车不进入本轮选车。
List<VehicleSnapshot> candidates = new ArrayList<VehicleSnapshot>();
Map<String, List<String>> approaches = new LinkedHashMap<String, List<String>>();
Map<String, RouteCandidateSet> approaches =
new LinkedHashMap<String, RouteCandidateSet>();
for (SimulatedVehicleRuntime vehicle : vehicles.values()) {
if (!vehicle.isAssignmentEligible() || active.containsKey(vehicle.getVehicleId())
|| clearanceMoves.containsKey(vehicle.getVehicleId())) {
continue;
}
List<String> approach = planner.shortestPath(input.getGridMap(),
vehicle.getCurrentSite(), task.getPhysicalSource(), Collections.emptySet());
if (!approach.isEmpty()) {
RouteCandidateSet approach = routePlanner.plan(input.getGridMap(),
vehicle.getCurrentSite(), task.getPhysicalSource(),
Collections.<String>emptySet(), RoutePlanningPolicy.defaultPolicy());
if (!approach.getCandidates().isEmpty()) {
candidates.add(vehicle.snapshot());
approaches.put(vehicle.getVehicleId(), approach);
}
}
if (candidates.isEmpty()) {
// 暂无可分配车不等于失败;只有全车队都无法抵达源点时才关闭为 NO_ROUTE。
if (!fleetCanReach(task, vehicles, input, planner)) {
if (!fleetCanReach(task, vehicles, input, routePlanner)) {
queue.remove(task.getSimulationTaskId());
terminal.put(task.getSimulationTaskId(), result(executions.get(
task.getSimulationTaskId()), SimulationTaskStatus.FAILED,
@@ -343,8 +359,17 @@ public final class HistoricalDispatchSimulator {
if (!decision.getVehicle().isPresent()) continue;
String vehicleId = decision.getVehicle().get().getVehicleId();
// 车辆先走到取货/起始点,再沿新规划的服务路径到达任务终点,形成实际执行路线。
List<String> route = new ArrayList<String>(approaches.get(vehicleId));
// 所有候选共用这一刻的占点、边预约、走廊方向和活动路线,保证比较口径一致。
TrafficRouteSnapshot traffic = trafficSnapshot(
vehicles, active, ledger, reservations);
RoutePlanDecision approachDecision = routeEvaluator.select(input.getGridMap(),
approaches.get(vehicleId), traffic, task.getSimulationTaskId(), vehicleId);
RoutePlanDecision serviceDecision = routeEvaluator.select(input.getGridMap(),
serviceCandidates, traffic, task.getSimulationTaskId(), vehicleId);
// 车辆先走到业务源点,再执行服务段;拼接时去掉重复源点。
List<String> route = new ArrayList<String>(approachDecision.getSelectedPath());
List<String> servicePath = serviceDecision.getSelectedPath();
for (int i = 1; i < servicePath.size(); i++) route.add(servicePath.get(i));
Execution execution = executions.get(task.getSimulationTaskId());
@@ -365,6 +390,13 @@ public final class HistoricalDispatchSimulator {
execution.lastVehicleId = vehicleId;
execution.plannedPath = new ArrayList<String>(servicePath);
execution.assignedVehicles.add(vehicleId);
execution.pathVersion++;
execution.planningEvidence.add(new RoutePlanningEvidence(
RoutePlanningEvidence.PlanningStage.APPROACH,
execution.pathVersion, now, false, approachDecision));
execution.planningEvidence.add(new RoutePlanningEvidence(
RoutePlanningEvidence.PlanningStage.SERVICE,
execution.pathVersion, now, false, serviceDecision));
active.put(vehicleId, new ActiveTask(execution, route));
queue.remove(task.getSimulationTaskId());
events.add(event(tick, now, "ASSIGN", task.getSimulationTaskId(), vehicleId,
@@ -387,14 +419,52 @@ public final class HistoricalDispatchSimulator {
private boolean fleetCanReach(HistoricalSimulationTask task,
Map<String, SimulatedVehicleRuntime> vehicles,
HistoricalSimulationInput input,
GridPathPlanner planner) {
RouteCandidatePlanner planner) {
for (SimulatedVehicleRuntime vehicle : vehicles.values()) {
if (!planner.shortestPath(input.getGridMap(), vehicle.getCurrentSite(),
task.getPhysicalSource(), Collections.emptySet()).isEmpty()) return true;
if (!planner.plan(input.getGridMap(), vehicle.getCurrentSite(),
task.getPhysicalSource(), Collections.<String>emptySet(),
RoutePlanningPolicy.defaultPolicy()).getCandidates().isEmpty()) return true;
}
return false;
}
/**
* 冻结本次规划看到的车辆占点、已预约方向、权威边资源和其他活动任务剩余路线。
*/
private TrafficRouteSnapshot trafficSnapshot(
Map<String, SimulatedVehicleRuntime> vehicles,
Map<String, ActiveTask> active,
ResourceLedger ledger,
CorridorReservationManager reservations) {
TrafficRouteSnapshot.Builder snapshot = TrafficRouteSnapshot.builder();
for (SimulatedVehicleRuntime vehicle : vehicles.values()) {
snapshot.occupyNode(vehicle.getCurrentSite(), vehicle.getVehicleId());
}
for (Map.Entry<String, ActiveTask> entry : active.entrySet()) {
ActiveTask task = entry.getValue();
List<String> remaining = task.route.subList(task.index, task.route.size());
snapshot.activeRoute(task.execution.task.getSimulationTaskId(), remaining);
// 只有台账中确实由该活动车辆持有的边才属于当前瞬时独占资源。
for (int index = 1; index < remaining.size(); index++) {
String from = remaining.get(index - 1);
String to = remaining.get(index);
if (ledger.holdersFor(ResourceKey.edge(from, to)).contains(entry.getKey())) {
snapshot.reserveEdge(from, to,
task.execution.task.getSimulationTaskId());
}
}
}
for (CorridorReservationManager.CorridorReservationSnapshot reservation
: reservations.snapshot()) {
for (String taskId : reservation.getTaskIds()) {
snapshot.reserveDirection(reservation.getFromSite(),
reservation.getToSite(), taskId);
}
}
return snapshot.build();
}
/**
* 推进全部已分配任务的下一条边,并在移动前依次执行方向冲突、清障、交通决策、资源台账和 PLC 确认。
*
@@ -920,7 +990,8 @@ public final class HistoricalDispatchSimulator {
status == SimulationTaskStatus.COMPLETED ? now : null,
execution.holdCount,
plc.attemptsFor(execution.task.getSimulationTaskId()),
execution.retryCount, execution.holdReasons, execution.retryReasons);
execution.retryCount, execution.holdReasons, execution.retryReasons,
execution.planningEvidence);
}
/**
@@ -1004,6 +1075,9 @@ public final class HistoricalDispatchSimulator {
new LinkedHashMap<String, Integer>();
private final Map<String, Integer> retryReasons =
new LinkedHashMap<String, Integer>();
private final List<RoutePlanningEvidence> planningEvidence =
new ArrayList<RoutePlanningEvidence>();
private int pathVersion;
/**
* 创建 {@code Execution} 实例并初始化其内部状态。

View File

@@ -0,0 +1,51 @@
package com.ft546.wcs.shadow.history.simulation;
import com.ft546.wcs.demo.traffic.RoutePlanDecision;
/** 保存一次接近段或服务段路线选择的不可变业务证据。 */
public final class RoutePlanningEvidence {
public enum PlanningStage { APPROACH, SERVICE }
private final PlanningStage stage;
private final int pathVersion;
private final long simulationMillis;
private final boolean reroute;
private final RoutePlanDecision decision;
/**
* 记录一次已实际采用的中心规划决定。
*
* @param stage 车辆到源点的接近段或源点到目标的服务段
* @param pathVersion 任务执行路线版本,从 1 开始递增
* @param simulationMillis 作出决定的仿真时间
* @param reroute 是否由连续交通 HOLD 触发
* @param decision 候选集合、质量元组和最终选择
*/
public RoutePlanningEvidence(PlanningStage stage, int pathVersion,
long simulationMillis, boolean reroute,
RoutePlanDecision decision) {
if (stage == null || pathVersion <= 0 || simulationMillis < 0L || decision == null) {
throw new IllegalArgumentException("valid route planning evidence is required");
}
this.stage = stage;
this.pathVersion = pathVersion;
this.simulationMillis = simulationMillis;
this.reroute = reroute;
this.decision = decision;
}
/** 返回本次决定对应接近段还是服务段。 */
public PlanningStage getStage() { return stage; }
/** 返回采用该决定后的完整执行路线版本。 */
public int getPathVersion() { return pathVersion; }
/** 返回本次决定发生的仿真时间。 */
public long getSimulationMillis() { return simulationMillis; }
/** 返回该决定是否来自运行中的交通重规划。 */
public boolean isReroute() { return reroute; }
/** 返回候选、质量和选择原因。 */
public RoutePlanDecision getDecision() { return decision; }
}

View File

@@ -24,6 +24,7 @@ public final class SimulationTaskResult {
private final int retryCount;
private final Map<String, Integer> holdReasons;
private final Map<String, Integer> retryReasons;
private final List<RoutePlanningEvidence> planningEvidence;
/**
* 创建 {@code SimulationTaskResult} 实例并初始化其内部状态。
@@ -44,6 +45,7 @@ public final class SimulationTaskResult {
* @param retryCount 执行该处理所需的 retry count 输入。
* @param holdReasons 执行该处理所需的 hold reasons 输入。
* @param retryReasons 执行该处理所需的 retry reasons 输入。
* @param planningEvidence 每个实际采用路径版本的接近段和服务段决策证据。
*/
SimulationTaskResult(String simulationTaskId, HistoricalQueueRecord record,
SimulationTaskStatus status, String reason,
@@ -52,7 +54,8 @@ public final class SimulationTaskResult {
Long firstAssignedMillis, Long completedMillis,
int holdCount, int protocolAttempts, int retryCount,
Map<String, Integer> holdReasons,
Map<String, Integer> retryReasons) {
Map<String, Integer> retryReasons,
List<RoutePlanningEvidence> planningEvidence) {
this.simulationTaskId = simulationTaskId;
this.record = record;
this.status = status;
@@ -68,6 +71,7 @@ public final class SimulationTaskResult {
this.retryCount = retryCount;
this.holdReasons = immutableMap(holdReasons);
this.retryReasons = immutableMap(retryReasons);
this.planningEvidence = immutable(planningEvidence);
}
/**
@@ -175,6 +179,9 @@ public final class SimulationTaskResult {
* @return 本次处理产生的结果;调用方据此继续后续流程。
*/
public Map<String, Integer> getRetryReasons() { return retryReasons; }
/** 返回任务每个路径版本的接近段和服务段中心规划证据。 */
public List<RoutePlanningEvidence> getPlanningEvidence() { return planningEvidence; }
/**
* 返回当前对象保存的“waiting millis”信息。
*

View File

@@ -78,11 +78,34 @@ class HistoricalDispatchSimulatorTest {
alternateRouteInput(), SimulationConfig.defaults(),
DeterministicFaultPlan.none());
assertEquals(Arrays.asList("A", "C"),
assertEquals(Arrays.asList("A", "B", "C"),
result.getTaskResults().get(0).getPlannedPath());
assertEquals(1, result.getReplannedCount());
}
@Test
void selectsBoundedConflictFreeServiceRouteAndStoresBothPlanningStages() {
HistoricalDispatchSimulationResult result = new HistoricalDispatchSimulator().simulate(
conflictAwareRoutingInput(), new SimulationConfig(100, 30_000L, 3, 100, 300),
DeterministicFaultPlan.none());
SimulationTaskResult second = result.getTaskResults().stream()
.filter(task -> "SAFE-2".equals(task.getSimulationTaskId()))
.findFirst().orElseThrow(AssertionError::new);
assertEquals(Arrays.asList("D", "C", "A"), second.getPlannedPath());
assertEquals(2, second.getPlanningEvidence().size());
RoutePlanningEvidence approach = second.getPlanningEvidence().get(0);
RoutePlanningEvidence service = second.getPlanningEvidence().get(1);
assertEquals(RoutePlanningEvidence.PlanningStage.APPROACH, approach.getStage());
assertEquals(RoutePlanningEvidence.PlanningStage.SERVICE, service.getStage());
assertEquals(1, service.getPathVersion());
assertEquals("FEWER_OPPOSITE_DIRECTION_CONFLICTS",
service.getDecision().getDecisionReason());
assertTrue(service.getDecision().getDetourRatio() <= 1.10d);
assertEquals(2, service.getDecision().getCandidateSet().getCandidates().size());
assertEquals(0, result.getFailedCount());
}
/**
* 验证“moves idle blockers node by node and completes both business directions”场景,确保相关业务规则和断言成立。
*
@@ -211,13 +234,46 @@ class HistoricalDispatchSimulatorTest {
"test", "test", nodes, edges, Collections.emptyMap(),
Collections.emptySet(), Collections.emptySet()));
HistoricalSimulationTask task = customTask(
"R1", "A", "C", 1, 0, Arrays.asList("A", "B", "C"));
"R1", "A", "C", 1, 0, Arrays.asList("A", "C"));
return new HistoricalSimulationInput(Collections.singletonList(task),
Collections.emptyList(), grid, traffic,
Collections.singletonList(new VehicleSnapshot(
"1", 1, "A", 5, 1, 100, true, true, false)));
}
private HistoricalSimulationInput conflictAwareRoutingInput() {
GridMap grid = new GridMap();
grid.addSite(new Site("A", 1, 0, 0, "TEST"));
grid.addSite(new Site("B", 1, 5, 0, "TEST"));
grid.addSite(new Site("C", 1, 5, 1, "TEST"));
grid.addSite(new Site("D", 1, 10, 0, "TEST"));
grid.connect("A", "B");
grid.connect("B", "D");
grid.connect("A", "C");
grid.connect("C", "D");
LinkedHashSet<String> nodes = new LinkedHashSet<String>(
Arrays.asList("A", "B", "C", "D"));
LinkedHashSet<TrafficMapCompiler.EdgeDefinition> edges =
new LinkedHashSet<TrafficMapCompiler.EdgeDefinition>();
edges.add(new TrafficMapCompiler.EdgeDefinition("A", "B"));
edges.add(new TrafficMapCompiler.EdgeDefinition("B", "D"));
edges.add(new TrafficMapCompiler.EdgeDefinition("A", "C"));
edges.add(new TrafficMapCompiler.EdgeDefinition("C", "D"));
EffectiveTrafficMap traffic = new TrafficMapCompiler().compile(
new TrafficMapCompiler.MapDefinition("CONFLICT-AWARE", 1L, "TEST", true,
"test", "test", nodes, edges, Collections.emptyMap(),
Collections.emptySet(), Collections.emptySet()));
List<HistoricalSimulationTask> tasks = Arrays.asList(
customTask("SAFE-1", "A", "D", 1, 0, Arrays.asList("A", "B", "D")),
customTask("SAFE-2", "D", "A", 2, 0, Arrays.asList("D", "B", "A")));
List<VehicleSnapshot> vehicles = Arrays.asList(
new VehicleSnapshot("1", 1, "A", 5, 1, 100, true, true, false),
new VehicleSnapshot("2", 1, "D", 5, 1, 100, true, true, false));
return new HistoricalSimulationInput(tasks, Collections.emptyList(), grid,
traffic, vehicles);
}
/**
* 验证“blocked three vehicle corridor input”场景,确保相关业务规则和断言成立。
*