diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/HistoricalDispatchSimulator.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/HistoricalDispatchSimulator.java index 99bf31c..d7dc926 100644 --- a/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/HistoricalDispatchSimulator.java +++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/HistoricalDispatchSimulator.java @@ -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 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 traversedEdges = new LinkedHashSet(); 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 clearanceMoves, Map executions, Map terminal, - GridPathPlanner planner, + RouteCandidatePlanner routePlanner, + RouteQualityEvaluator routeEvaluator, VehicleAssignmentService assignment, SimulatedPlcGateway plc, + ResourceLedger ledger, CorridorReservationManager reservations, List 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 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.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 candidates = new ArrayList(); - Map> approaches = new LinkedHashMap>(); + Map approaches = + new LinkedHashMap(); for (SimulatedVehicleRuntime vehicle : vehicles.values()) { if (!vehicle.isAssignmentEligible() || active.containsKey(vehicle.getVehicleId()) || clearanceMoves.containsKey(vehicle.getVehicleId())) { continue; } - List 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.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 route = new ArrayList(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 route = new ArrayList(approachDecision.getSelectedPath()); + List 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(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 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.emptySet(), + RoutePlanningPolicy.defaultPolicy()).getCandidates().isEmpty()) return true; } return false; } + /** + * 冻结本次规划看到的车辆占点、已预约方向、权威边资源和其他活动任务剩余路线。 + */ + private TrafficRouteSnapshot trafficSnapshot( + Map vehicles, + Map active, + ResourceLedger ledger, + CorridorReservationManager reservations) { + TrafficRouteSnapshot.Builder snapshot = TrafficRouteSnapshot.builder(); + for (SimulatedVehicleRuntime vehicle : vehicles.values()) { + snapshot.occupyNode(vehicle.getCurrentSite(), vehicle.getVehicleId()); + } + for (Map.Entry entry : active.entrySet()) { + ActiveTask task = entry.getValue(); + List 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(); private final Map retryReasons = new LinkedHashMap(); + private final List planningEvidence = + new ArrayList(); + private int pathVersion; /** * 创建 {@code Execution} 实例并初始化其内部状态。 diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/RoutePlanningEvidence.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/RoutePlanningEvidence.java new file mode 100644 index 0000000..87e0a39 --- /dev/null +++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/RoutePlanningEvidence.java @@ -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; } +} diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/SimulationTaskResult.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/SimulationTaskResult.java index a3dec14..74ca692 100644 --- a/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/SimulationTaskResult.java +++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/simulation/SimulationTaskResult.java @@ -24,6 +24,7 @@ public final class SimulationTaskResult { private final int retryCount; private final Map holdReasons; private final Map retryReasons; + private final List 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 holdReasons, - Map retryReasons) { + Map retryReasons, + List 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 getRetryReasons() { return retryReasons; } + + /** 返回任务每个路径版本的接近段和服务段中心规划证据。 */ + public List getPlanningEvidence() { return planningEvidence; } /** * 返回当前对象保存的“waiting millis”信息。 * diff --git a/javaTest/src/test/java/com/ft546/wcs/shadow/history/simulation/HistoricalDispatchSimulatorTest.java b/javaTest/src/test/java/com/ft546/wcs/shadow/history/simulation/HistoricalDispatchSimulatorTest.java index 4f460f0..44c49d5 100644 --- a/javaTest/src/test/java/com/ft546/wcs/shadow/history/simulation/HistoricalDispatchSimulatorTest.java +++ b/javaTest/src/test/java/com/ft546/wcs/shadow/history/simulation/HistoricalDispatchSimulatorTest.java @@ -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 nodes = new LinkedHashSet( + Arrays.asList("A", "B", "C", "D")); + LinkedHashSet edges = + new LinkedHashSet(); + 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 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 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”场景,确保相关业务规则和断言成立。 *