feat(javaTest): orchestrate cross-floor dispatch
This commit is contained in:
@@ -32,6 +32,42 @@ public final class ClearanceMovePlanner {
|
||||
Set<String> occupiedSites,
|
||||
Set<ResourceKey> reservedResources,
|
||||
GridMap map) {
|
||||
return planInternal(vehicleId, currentSite, ownerTaskId, ownerRemainingRoute,
|
||||
occupiedSites, reservedResources, map, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在父业务路线尚未预约和激活之前,把占住其终点的空闲车移到路线外的安全支路。
|
||||
*
|
||||
* <p>与活动任务清障不同,此时父车辆尚未进入通道,所以清障路径可以暂时经过未来业务路线;但是
|
||||
* 清障终点仍必须位于该路线之外,避免只把阻塞车从终点挪到前置节点后再次挡住父任务。调用方必须
|
||||
* 等清障完成后才能提交父路线。
|
||||
*
|
||||
* @param vehicleId 占住业务终点、需要先让位的车辆。
|
||||
* @param currentSite 当前被占用的业务终点。
|
||||
* @param ownerTaskId 等待取得路线承诺的父任务。
|
||||
* @param ownerRoute 父任务准备提交但尚未预约的完整路线。
|
||||
* @param occupiedSites 全车队当前占点。
|
||||
* @param reservedResources 已由其他物理动作持有的资源。
|
||||
* @param map 当前项目路网。
|
||||
* @return 能把车辆停到父路线外时返回逐节点清障动作,否则返回空。
|
||||
*/
|
||||
public Optional<ClearanceMove> planBeforeRouteCommit(
|
||||
String vehicleId, String currentSite, String ownerTaskId,
|
||||
List<String> ownerRoute, Set<String> occupiedSites,
|
||||
Set<ResourceKey> reservedResources, GridMap map) {
|
||||
return planInternal(vehicleId, currentSite, ownerTaskId, ownerRoute,
|
||||
occupiedSites, reservedResources, map, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一选择清障终点;只有准入前清障可以穿过尚未生效的父路线。
|
||||
*/
|
||||
private Optional<ClearanceMove> planInternal(
|
||||
String vehicleId, String currentSite, String ownerTaskId,
|
||||
List<String> ownerRemainingRoute, Set<String> occupiedSites,
|
||||
Set<ResourceKey> reservedResources, GridMap map,
|
||||
boolean mayTraverseOwnerRoute) {
|
||||
if (vehicleId == null || currentSite == null || ownerTaskId == null
|
||||
|| ownerRemainingRoute == null || occupiedSites == null
|
||||
|| reservedResources == null || map == null) {
|
||||
@@ -47,6 +83,19 @@ public final class ClearanceMovePlanner {
|
||||
}
|
||||
excluded.remove(currentSite);
|
||||
|
||||
// 准入前父路线还没有形成物理承诺,只限制清障终点;真正的占车和节点资源仍是路径硬障碍。
|
||||
Set<String> pathObstacles = new LinkedHashSet<String>(excluded);
|
||||
if (mayTraverseOwnerRoute) {
|
||||
pathObstacles.removeAll(ownerRemainingRoute);
|
||||
pathObstacles.addAll(occupiedSites);
|
||||
for (ResourceKey resource : reservedResources) {
|
||||
if (resource.value().startsWith("NODE:")) {
|
||||
pathObstacles.add(resource.value().substring("NODE:".length()));
|
||||
}
|
||||
}
|
||||
pathObstacles.remove(currentSite);
|
||||
}
|
||||
|
||||
GridPathPlanner planner = new GridPathPlanner();
|
||||
List<Candidate> candidates = new ArrayList<Candidate>();
|
||||
// 枚举全图而不是只看邻居,使被阻塞车辆能找到最近的可达让路站点。
|
||||
@@ -54,7 +103,8 @@ public final class ClearanceMovePlanner {
|
||||
Collections.sort(siteIds);
|
||||
for (String target : siteIds) {
|
||||
if (target.equals(currentSite) || excluded.contains(target)) continue;
|
||||
List<String> route = planner.shortestPath(map, currentSite, target, excluded);
|
||||
List<String> route = planner.shortestPath(
|
||||
map, currentSite, target, pathObstacles);
|
||||
if (!route.isEmpty()) {
|
||||
candidates.add(new Candidate(target, route,
|
||||
map.getNeighbors(target).size() <= 1));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@ public final class HistoricalSimulationInput {
|
||||
private final GridMap gridMap;
|
||||
private final EffectiveTrafficMap trafficMap;
|
||||
private final List<VehicleSnapshot> initialVehicles;
|
||||
private final FloorTransferTopology floorTransferTopology;
|
||||
|
||||
/**
|
||||
* 创建 {@code HistoricalSimulationInput} 实例并初始化其内部状态。
|
||||
@@ -29,11 +30,36 @@ public final class HistoricalSimulationInput {
|
||||
List<HistoricalSimulationFailure> failedInputs,
|
||||
GridMap gridMap, EffectiveTrafficMap trafficMap,
|
||||
List<VehicleSnapshot> initialVehicles) {
|
||||
this(tasks, failedInputs, gridMap, trafficMap, initialVehicles,
|
||||
new FloorTransferTopology(Collections.<Integer, String>emptyMap()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带项目换层端口目录的仿真输入。
|
||||
*
|
||||
* <p>换层端口与普通地图边分开保存。中心调度只能通过该目录生成换层作业,不能在
|
||||
* {@link GridMap} 中虚构跨层边;没有配置端口的测试输入继续只支持同层调度。
|
||||
* @param tasks 可按 create_date 进入调度队列的业务任务。
|
||||
* @param failedInputs 输入阶段已经关闭的非法历史记录。
|
||||
* @param gridMap 只包含同层物理邻接关系的完整地图。
|
||||
* @param trafficMap 节点、边和交通资源容量定义。
|
||||
* @param initialVehicles 本次仿真的初始车队快照。
|
||||
* @param floorTransferTopology 项目楼层到换层交接站点的独立配置。
|
||||
*/
|
||||
HistoricalSimulationInput(List<HistoricalSimulationTask> tasks,
|
||||
List<HistoricalSimulationFailure> failedInputs,
|
||||
GridMap gridMap, EffectiveTrafficMap trafficMap,
|
||||
List<VehicleSnapshot> initialVehicles,
|
||||
FloorTransferTopology floorTransferTopology) {
|
||||
this.tasks = immutable(tasks);
|
||||
this.failedInputs = immutable(failedInputs);
|
||||
this.gridMap = gridMap;
|
||||
this.trafficMap = trafficMap;
|
||||
this.initialVehicles = immutable(initialVehicles);
|
||||
if (floorTransferTopology == null) {
|
||||
throw new IllegalArgumentException("floor transfer topology is required");
|
||||
}
|
||||
this.floorTransferTopology = floorTransferTopology;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,6 +98,9 @@ public final class HistoricalSimulationInput {
|
||||
*/
|
||||
public List<VehicleSnapshot> getInitialVehicles() { return initialVehicles; }
|
||||
|
||||
/** 返回项目换层端口目录;该目录不向普通路径规划器暴露跨层边。 */
|
||||
public FloorTransferTopology getFloorTransferTopology() { return floorTransferTopology; }
|
||||
|
||||
/**
|
||||
* 执行“all simulation ids”所表示的处理流程。
|
||||
*
|
||||
|
||||
@@ -10,7 +10,9 @@ import com.ft546.wcs.shadow.history.HistoricalQueueRecord;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class HistoricalSimulationInputFactory {
|
||||
/**
|
||||
@@ -75,7 +77,24 @@ public final class HistoricalSimulationInputFactory {
|
||||
|
||||
List<VehicleSnapshot> vehicles = initialVehicles(compiled);
|
||||
return new HistoricalSimulationInput(tasks, failures, compiled.getGridMap(),
|
||||
compiled.getTrafficMap(), vehicles);
|
||||
compiled.getTrafficMap(), vehicles, floorTransferTopology(compiled));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 FT546 现有业务别名规则,把每层 {@code 07050<floor>} 登记为换层交接站点。
|
||||
*
|
||||
* <p>这里只建立项目设备端口配置,不调用 {@code GridMap.connect},因此路径规划器仍然看不到
|
||||
* 任何普通跨层边。某层没有该真实站点时保持未配置,让中心调度明确判定跨层不可用。
|
||||
*/
|
||||
private FloorTransferTopology floorTransferTopology(
|
||||
HistoricalGridMapCompiler.CompiledHistoricalMap compiled) {
|
||||
Map<Integer, String> ports = new LinkedHashMap<Integer, String>();
|
||||
for (int floor = 1; floor <= 3; floor++) {
|
||||
String siteId = "07050" + floor;
|
||||
HistoricalMapCell cell = compiled.getPassableSites().get(siteId);
|
||||
if (cell != null && cell.getZ() == floor) ports.put(floor, siteId);
|
||||
}
|
||||
return new FloorTransferTopology(ports);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -51,7 +51,8 @@ public final class PointToPointSimulationInputFactory {
|
||||
CREATED_AT.plusMinutes(1), sourceSite, targetSite, history, 0);
|
||||
return new HistoricalSimulationInput(
|
||||
Collections.singletonList(task), Collections.emptyList(),
|
||||
base.getGridMap(), base.getTrafficMap(), base.getInitialVehicles());
|
||||
base.getGridMap(), base.getTrafficMap(), base.getInitialVehicles(),
|
||||
base.getFloorTransferTopology());
|
||||
}
|
||||
|
||||
private String taskTypeCode(DispatchTask.TaskType taskType) {
|
||||
|
||||
@@ -26,6 +26,8 @@ public final class SimulationTaskResult {
|
||||
private final Map<String, Integer> retryReasons;
|
||||
private final List<RoutePlanningEvidence> planningEvidence;
|
||||
private final int rerouteCount;
|
||||
private final AssignmentMode assignmentMode;
|
||||
private final List<FloorTransferJob> floorTransferJobs;
|
||||
|
||||
/**
|
||||
* 创建 {@code SimulationTaskResult} 实例并初始化其内部状态。
|
||||
@@ -48,6 +50,8 @@ public final class SimulationTaskResult {
|
||||
* @param retryReasons 执行该处理所需的 retry reasons 输入。
|
||||
* @param planningEvidence 每个实际采用路径版本的接近段和服务段决策证据。
|
||||
* @param rerouteCount 运行中成功替换剩余路线的次数。
|
||||
* @param assignmentMode 中心采用的直接派车或先换层编排模式;未分配失败任务为 {@code null}。
|
||||
* @param floorTransferJobs 父任务实际创建过的幂等换层作业证据。
|
||||
*/
|
||||
SimulationTaskResult(String simulationTaskId, HistoricalQueueRecord record,
|
||||
SimulationTaskStatus status, String reason,
|
||||
@@ -58,7 +62,8 @@ public final class SimulationTaskResult {
|
||||
Map<String, Integer> holdReasons,
|
||||
Map<String, Integer> retryReasons,
|
||||
List<RoutePlanningEvidence> planningEvidence,
|
||||
int rerouteCount) {
|
||||
int rerouteCount, AssignmentMode assignmentMode,
|
||||
List<FloorTransferJob> floorTransferJobs) {
|
||||
this.simulationTaskId = simulationTaskId;
|
||||
this.record = record;
|
||||
this.status = status;
|
||||
@@ -76,6 +81,8 @@ public final class SimulationTaskResult {
|
||||
this.retryReasons = immutableMap(retryReasons);
|
||||
this.planningEvidence = immutable(planningEvidence);
|
||||
this.rerouteCount = rerouteCount;
|
||||
this.assignmentMode = assignmentMode;
|
||||
this.floorTransferJobs = immutable(floorTransferJobs);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,6 +196,15 @@ public final class SimulationTaskResult {
|
||||
|
||||
/** 返回运行中因连续交通 HOLD 成功切换路线的次数。 */
|
||||
public int getRerouteCount() { return rerouteCount; }
|
||||
|
||||
/** 返回中心最终采用的直接派车或先换层编排模式。 */
|
||||
public AssignmentMode getAssignmentMode() { return assignmentMode; }
|
||||
|
||||
/** 返回该父任务实际创建过的全部换层作业,供幂等和重派审计。 */
|
||||
public List<FloorTransferJob> getFloorTransferJobs() { return floorTransferJobs; }
|
||||
|
||||
/** 返回该父任务产生的换层业务效果数;重复提交不会增加该计数。 */
|
||||
public int getFloorTransferCount() { return floorTransferJobs.size(); }
|
||||
/**
|
||||
* 返回当前对象保存的“waiting millis”信息。
|
||||
*
|
||||
|
||||
@@ -39,4 +39,28 @@ class ClearanceMovePlannerTest {
|
||||
assertEquals(Arrays.asList("B", "X"), planned.get().getRoute());
|
||||
assertEquals("OWNER", planned.get().getOwnerTaskId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证业务路线尚未取得预约时,终点占车可以沿未来路线退出,但最终必须停到路线外的安全支路。
|
||||
*/
|
||||
@Test
|
||||
void clearsADestinationBlockerThroughTheUncommittedRouteToAnOffRouteBranch() {
|
||||
GridMap map = new GridMap();
|
||||
for (String site : Arrays.asList("L", "M", "R", "Y")) {
|
||||
map.addSite(new Site(site, 1, site.charAt(0), 1, "TEST"));
|
||||
}
|
||||
map.connect("L", "M");
|
||||
map.connect("M", "R");
|
||||
map.connect("M", "Y");
|
||||
|
||||
Optional<ClearanceMove> planned = new ClearanceMovePlanner()
|
||||
.planBeforeRouteCommit(
|
||||
"2", "R", "SWAP", Arrays.asList("L", "M", "R"),
|
||||
new LinkedHashSet<String>(Arrays.asList("L", "R")),
|
||||
Collections.<ResourceKey>emptySet(), map);
|
||||
|
||||
assertTrue(planned.isPresent());
|
||||
assertEquals(Arrays.asList("R", "M", "Y"), planned.get().getRoute());
|
||||
assertEquals("Y", planned.get().getTargetSite());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,10 @@ import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.ft546.wcs.shadow.history.simulation.SimulatedPlcGateway.PlcReason.SEND_FAILURE;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@@ -80,9 +82,147 @@ class HistoricalDispatchSimulatorTest {
|
||||
|
||||
assertEquals(Arrays.asList("A", "B", "C"),
|
||||
result.getTaskResults().get(0).getPlannedPath());
|
||||
assertEquals(AssignmentMode.DIRECT,
|
||||
result.getTaskResults().get(0).getAssignmentMode());
|
||||
assertEquals(0, result.getTaskResults().get(0).getFloorTransferCount());
|
||||
assertEquals(1, result.getReplannedCount());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证中心调度不会因普通地图没有跨层边而排除车辆,而是选车后创建换层作业并从实际出口重规划。
|
||||
*/
|
||||
@Test
|
||||
void transfersACrossFloorVehicleThenReplansFromTheActualExit() {
|
||||
SimulatedFloorTransferGateway gateway = new SimulatedFloorTransferGateway();
|
||||
HistoricalDispatchSimulationResult result = new HistoricalDispatchSimulator().simulate(
|
||||
crossFloorInput(true), new SimulationConfig(100, 30_000L, 3, 100, 100),
|
||||
DeterministicFaultPlan.none(), gateway);
|
||||
|
||||
SimulationTaskResult task = result.getTaskResults().get(0);
|
||||
assertEquals(SimulationTaskStatus.COMPLETED, task.getStatus());
|
||||
assertEquals(AssignmentMode.FLOOR_TRANSFER, task.getAssignmentMode());
|
||||
assertEquals(1, task.getFloorTransferCount());
|
||||
assertEquals(1, task.getFloorTransferJobs().size());
|
||||
FloorTransferJob job = task.getFloorTransferJobs().get(0);
|
||||
assertEquals("1", job.getVehicleId());
|
||||
assertEquals(1, job.getSourceFloor());
|
||||
assertEquals(2, job.getTargetFloor());
|
||||
assertEquals("LIFT-1", job.getSourceSite());
|
||||
assertEquals("LIFT-2", job.getTargetSite());
|
||||
assertEquals(Arrays.asList("A", "B"), task.getPlannedPath());
|
||||
|
||||
assertTrue(result.getEvents().stream().anyMatch(event ->
|
||||
"FLOOR_TRANSFER_SUBMITTED".equals(event.getType())));
|
||||
assertTrue(result.getEvents().stream().anyMatch(event ->
|
||||
"FLOOR_TRANSFER_RUNNING".equals(event.getType())));
|
||||
assertTrue(result.getEvents().stream().anyMatch(event ->
|
||||
"FLOOR_TRANSFER_COMPLETED".equals(event.getType())
|
||||
&& "LIFT-1".equals(event.getFromSite())
|
||||
&& "LIFT-2".equals(event.getToSite())));
|
||||
assertTrue(result.getEvents().stream().anyMatch(event ->
|
||||
"POST_TRANSFER_REPLAN".equals(event.getType())
|
||||
&& "LIFT-2".equals(event.getFromSite())
|
||||
&& "B".equals(event.getToSite())));
|
||||
assertEquals(1, result.getArchiveCount());
|
||||
assertEquals(1, result.getWmsBusinessEffects());
|
||||
assertEquals(1, gateway.createdJobCount());
|
||||
assertEquals(1, gateway.completedJobCount());
|
||||
assertEquals(0, result.getInvariants().totalSafetyViolations());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证换层作业提交后车辆断线和重启只暂停原作业,恢复同步后仍由同一车辆继续父任务。
|
||||
*/
|
||||
@Test
|
||||
void keepsTheVehicleLockedAcrossDisconnectAndRestartDuringTransfer() {
|
||||
DeterministicFaultPlan faults = DeterministicFaultPlan.builder()
|
||||
.vehicleEvent(1L, "1", DeterministicFaultPlan.VehicleEventType.DISCONNECT)
|
||||
.vehicleEvent(2L, "1", DeterministicFaultPlan.VehicleEventType.CONNECT)
|
||||
.vehicleEvent(3L, "1", DeterministicFaultPlan.VehicleEventType.RESTART)
|
||||
.vehicleEvent(4L, "1", DeterministicFaultPlan.VehicleEventType.SYNCHRONIZE)
|
||||
.build();
|
||||
|
||||
HistoricalDispatchSimulationResult result = new HistoricalDispatchSimulator().simulate(
|
||||
crossFloorInput(true), new SimulationConfig(100, 30_000L, 3, 100, 100),
|
||||
faults);
|
||||
|
||||
SimulationTaskResult task = result.getTaskResults().get(0);
|
||||
assertEquals(SimulationTaskStatus.COMPLETED, task.getStatus());
|
||||
assertEquals(Collections.singletonList("1"), task.getAssignedVehicles());
|
||||
assertEquals(1, task.getFloorTransferCount());
|
||||
assertTrue(result.getEvents().stream().anyMatch(event ->
|
||||
"FLOOR_TRANSFER_HOLD".equals(event.getType())
|
||||
&& ("DISCONNECTED".equals(event.getReason())
|
||||
|| "RESTART_NOT_SYNCHRONIZED".equals(event.getReason()))));
|
||||
assertEquals(1L, result.getEvents().stream()
|
||||
.filter(event -> "FLOOR_TRANSFER_SUBMITTED".equals(event.getType()))
|
||||
.count());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证车辆锁属于中心调度不变量,即使项目适配器支持多台提升机并发,也不能给同一车辆创建第二个换层作业。
|
||||
*/
|
||||
@Test
|
||||
void doesNotAssignATransferLockedVehicleToASecondParentTask() {
|
||||
ParallelFloorTransferGateway gateway = new ParallelFloorTransferGateway();
|
||||
HistoricalDispatchSimulationResult result = new HistoricalDispatchSimulator().simulate(
|
||||
twoCrossFloorTasksInput(), new SimulationConfig(100, 30_000L, 3, 100, 200),
|
||||
DeterministicFaultPlan.none(), gateway);
|
||||
|
||||
assertEquals(2, result.getCompletedCount());
|
||||
assertEquals(1, gateway.submittedJobCount());
|
||||
assertEquals(1L, result.getEvents().stream()
|
||||
.filter(event -> "FLOOR_TRANSFER_SUBMITTED".equals(event.getType()))
|
||||
.count());
|
||||
assertEquals(0, result.getInvariants().totalSafetyViolations());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证目标出口已由普通任务承诺时,跨层作业必须在提交前 HOLD,不能先把车送入出口再形成等待环。
|
||||
*/
|
||||
@Test
|
||||
void waitsForTheTargetPortAndPostTransferCorridorBeforeSubmitting() {
|
||||
HistoricalDispatchSimulationResult result = new HistoricalDispatchSimulator().simulate(
|
||||
committedTransferExitInput(), new SimulationConfig(100, 30_000L, 3, 50, 200),
|
||||
DeterministicFaultPlan.builder()
|
||||
.vehicleEvent(3L, "1",
|
||||
DeterministicFaultPlan.VehicleEventType.DISCONNECT)
|
||||
.build());
|
||||
|
||||
assertEquals(2, result.getCompletedCount());
|
||||
long directCompletedAt = result.getEvents().stream()
|
||||
.filter(event -> "DIRECT-FIRST".equals(event.getTaskId())
|
||||
&& "COMPLETE".equals(event.getType()))
|
||||
.findFirst().orElseThrow(AssertionError::new).getSimulationMillis();
|
||||
long transferSubmittedAt = result.getEvents().stream()
|
||||
.filter(event -> "TRANSFER-SECOND".equals(event.getTaskId())
|
||||
&& "FLOOR_TRANSFER_SUBMITTED".equals(event.getType()))
|
||||
.findFirst().orElseThrow(AssertionError::new).getSimulationMillis();
|
||||
assertTrue(transferSubmittedAt >= directCompletedAt);
|
||||
assertTrue(result.getEvents().stream().anyMatch(event ->
|
||||
"TRANSFER-SECOND".equals(event.getTaskId())
|
||||
&& "FLOOR_TRANSFER_HOLD".equals(event.getType())
|
||||
&& ("TRANSFER_TARGET_NOT_PREPARED".equals(event.getReason())
|
||||
|| "CORRIDOR_DIRECTION_OWNED".equals(event.getReason()))));
|
||||
assertEquals(0, result.getInvariants().totalSafetyViolations());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证项目没有配置源层或目标层提升机端口时,跨层车辆不能被伪装成可达车辆。
|
||||
*/
|
||||
@Test
|
||||
void closesCrossFloorTaskAsNoRouteWhenTransferPortsAreMissing() {
|
||||
HistoricalDispatchSimulationResult result = new HistoricalDispatchSimulator().simulate(
|
||||
crossFloorInput(false), SimulationConfig.defaults(),
|
||||
DeterministicFaultPlan.none());
|
||||
|
||||
SimulationTaskResult task = result.getTaskResults().get(0);
|
||||
assertEquals(SimulationTaskStatus.FAILED, task.getStatus());
|
||||
assertEquals("NO_ROUTE", task.getReason());
|
||||
assertTrue(result.getEvents().stream().noneMatch(event ->
|
||||
event.getType().startsWith("FLOOR_TRANSFER")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectsBoundedConflictFreeServiceRouteAndStoresBothPlanningStages() {
|
||||
HistoricalDispatchSimulationResult result = new HistoricalDispatchSimulator().simulate(
|
||||
@@ -205,6 +345,90 @@ class HistoricalDispatchSimulatorTest {
|
||||
assertEquals(0, result.getInvariants().getCapacityViolations());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证单通道终点只允许一个尚未清空的业务承诺,避免跟车锁住首车唯一的离站方向。
|
||||
*
|
||||
* <p>首车完成入站后必须先执行从终点出发的离站任务;第二辆同向来车在此之前只能等待,不能提前
|
||||
* 持有整条反向走廊。若删除终点承诺门禁,跟车会停在 B、首车停在 C,离站任务因走廊反向预约
|
||||
* 无法分配,最终触发无物理进展看门狗。
|
||||
*/
|
||||
@Test
|
||||
void waitsForACommittedTerminalToClearBeforeDispatchingTheFollowingVehicle() {
|
||||
HistoricalDispatchSimulationResult result = new HistoricalDispatchSimulator().simulate(
|
||||
sharedDeadEndDestinationInput(),
|
||||
new SimulationConfig(100, 30_000L, 3, 40, 300),
|
||||
DeterministicFaultPlan.none());
|
||||
|
||||
assertEquals(3, result.getCompletedCount());
|
||||
assertEquals(0, result.getWaitingCount());
|
||||
assertEquals(0, result.getFailedCount());
|
||||
assertTrue(result.getEvents().stream().anyMatch(event ->
|
||||
"FOLLOW".equals(event.getTaskId())
|
||||
&& "HOLD".equals(event.getType())
|
||||
&& ("DESTINATION_COMMITTED".equals(event.getReason())
|
||||
|| "DESTINATION_OCCUPIED".equals(event.getReason()))));
|
||||
long departureCompletedAt = result.getEvents().stream()
|
||||
.filter(event -> "DEPART".equals(event.getTaskId())
|
||||
&& "COMPLETE".equals(event.getType()))
|
||||
.findFirst().orElseThrow(AssertionError::new).getSimulationMillis();
|
||||
long followerAssignedAt = result.getEvents().stream()
|
||||
.filter(event -> "FOLLOW".equals(event.getTaskId())
|
||||
&& "ASSIGN".equals(event.getType()))
|
||||
.findFirst().orElseThrow(AssertionError::new).getSimulationMillis();
|
||||
assertTrue(followerAssignedAt >= departureCompletedAt);
|
||||
assertEquals(0, result.getInvariants().totalSafetyViolations());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证所有车辆空闲但两个单通道终点互相占用时,中心先清出业务终点,再创建路线承诺。
|
||||
*/
|
||||
@Test
|
||||
void clearsAnOccupiedTerminalBeforeCommittingASwapTask() {
|
||||
HistoricalDispatchSimulationResult result = new HistoricalDispatchSimulator().simulate(
|
||||
occupiedTerminalSwapInput(),
|
||||
new SimulationConfig(100, 30_000L, 3, 40, 300),
|
||||
DeterministicFaultPlan.none());
|
||||
|
||||
assertEquals(2, result.getCompletedCount());
|
||||
assertEquals(0, result.getWaitingCount());
|
||||
assertTrue(result.getEvents().stream().anyMatch(event ->
|
||||
"CLEARANCE_PLANNED".equals(event.getType())
|
||||
&& "DESTINATION_BLOCKER".equals(event.getReason())));
|
||||
long clearanceCompletedAt = result.getEvents().stream()
|
||||
.filter(event -> "LEFT-TO-RIGHT".equals(event.getTaskId())
|
||||
&& "CLEARANCE_COMPLETE".equals(event.getType()))
|
||||
.findFirst().orElseThrow(AssertionError::new).getSimulationMillis();
|
||||
long ownerAssignmentAt = result.getEvents().stream()
|
||||
.filter(event -> "LEFT-TO-RIGHT".equals(event.getTaskId())
|
||||
&& "ASSIGN".equals(event.getType()))
|
||||
.findFirst().orElseThrow(AssertionError::new).getSimulationMillis();
|
||||
assertTrue(ownerAssignmentAt >= clearanceCompletedAt);
|
||||
assertEquals(0, result.getInvariants().totalSafetyViolations());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证准入前清障不会把车辆停到另一条活动任务已经承诺、但当前暂时空闲的最终停靠点。
|
||||
*/
|
||||
@Test
|
||||
void keepsDestinationClearanceAwayFromAnotherTasksCommittedTerminal() {
|
||||
HistoricalDispatchSimulationResult result = new HistoricalDispatchSimulator().simulate(
|
||||
clearanceAgainstCommittedTerminalInput(),
|
||||
new SimulationConfig(100, 30_000L, 3, 40, 300),
|
||||
DeterministicFaultPlan.none());
|
||||
|
||||
assertEquals(2, result.getCompletedCount());
|
||||
SimulationTickEvent clearance = result.getEvents().stream()
|
||||
.filter(event -> "SWAP".equals(event.getTaskId())
|
||||
&& "CLEARANCE_PLANNED".equals(event.getType()))
|
||||
.findFirst().orElseThrow(AssertionError::new);
|
||||
assertEquals("Y", clearance.getToSite());
|
||||
assertTrue(result.getEvents().stream().noneMatch(event ->
|
||||
"SWAP".equals(event.getTaskId())
|
||||
&& "CLEARANCE_PLANNED".equals(event.getType())
|
||||
&& "Z".equals(event.getToSite())));
|
||||
assertEquals(0, result.getInvariants().totalSafetyViolations());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证“controlled input”场景,确保相关业务规则和断言成立。
|
||||
*
|
||||
@@ -238,6 +462,113 @@ class HistoricalDispatchSimulatorTest {
|
||||
return new HistoricalSimulationInput(tasks, Collections.emptyList(), grid, traffic, vehicles);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 X-A-B-C 单通道并在 A 接出离站支路 D:ARRIVE 和 FOLLOW 都以 C 为终点,DEPART 把首车开往 D。
|
||||
*/
|
||||
private HistoricalSimulationInput sharedDeadEndDestinationInput() {
|
||||
GridMap grid = new GridMap();
|
||||
for (String node : Arrays.asList("X", "A", "B", "C", "D")) {
|
||||
grid.addSite(new Site(node, 1, node.charAt(0), 1, "TEST"));
|
||||
}
|
||||
grid.connect("X", "A");
|
||||
grid.connect("A", "B");
|
||||
grid.connect("B", "C");
|
||||
grid.connect("A", "D");
|
||||
|
||||
LinkedHashSet<String> nodes = new LinkedHashSet<String>(
|
||||
Arrays.asList("X", "A", "B", "C", "D"));
|
||||
LinkedHashSet<TrafficMapCompiler.EdgeDefinition> edges =
|
||||
new LinkedHashSet<TrafficMapCompiler.EdgeDefinition>();
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("X", "A"));
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("A", "B"));
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("B", "C"));
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("A", "D"));
|
||||
EffectiveTrafficMap traffic = new TrafficMapCompiler().compile(
|
||||
new TrafficMapCompiler.MapDefinition("SHARED-DEAD-END", 1L, "TEST", true,
|
||||
"test", "test", nodes, edges, Collections.emptyMap(),
|
||||
Collections.emptySet(), Collections.emptySet()));
|
||||
List<HistoricalSimulationTask> tasks = Arrays.asList(
|
||||
customTask("ARRIVE", "A", "C", 1, 0,
|
||||
Arrays.asList("A", "B", "C")),
|
||||
customTask("FOLLOW", "X", "C", 2, 0,
|
||||
Arrays.asList("X", "A", "B", "C")),
|
||||
customTask("DEPART", "C", "D", 0, 200,
|
||||
Arrays.asList("C", "B", "A", "D")));
|
||||
List<VehicleSnapshot> vehicles = Arrays.asList(
|
||||
new VehicleSnapshot("1", 1, "A", 5, 1, 100, true, true, false),
|
||||
new VehicleSnapshot("2", 1, "X", 5, 1, 100, true, true, false));
|
||||
return new HistoricalSimulationInput(tasks, Collections.emptyList(), grid,
|
||||
traffic, vehicles);
|
||||
}
|
||||
|
||||
/** 构造 L-M-R 主通道和 M-Y 支路,两辆车分别占住两个业务终点。 */
|
||||
private HistoricalSimulationInput occupiedTerminalSwapInput() {
|
||||
GridMap grid = new GridMap();
|
||||
for (String node : Arrays.asList("L", "M", "R", "Y")) {
|
||||
grid.addSite(new Site(node, 1, node.charAt(0), 1, "TEST"));
|
||||
}
|
||||
grid.connect("L", "M");
|
||||
grid.connect("M", "R");
|
||||
grid.connect("M", "Y");
|
||||
LinkedHashSet<String> nodes = new LinkedHashSet<String>(
|
||||
Arrays.asList("L", "M", "R", "Y"));
|
||||
LinkedHashSet<TrafficMapCompiler.EdgeDefinition> edges =
|
||||
new LinkedHashSet<TrafficMapCompiler.EdgeDefinition>();
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("L", "M"));
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("M", "R"));
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("M", "Y"));
|
||||
EffectiveTrafficMap traffic = new TrafficMapCompiler().compile(
|
||||
new TrafficMapCompiler.MapDefinition("OCCUPIED-SWAP", 1L, "TEST", true,
|
||||
"test", "test", nodes, edges, Collections.emptyMap(),
|
||||
Collections.emptySet(), Collections.emptySet()));
|
||||
List<HistoricalSimulationTask> tasks = Arrays.asList(
|
||||
customTask("LEFT-TO-RIGHT", "L", "R", 1, 0,
|
||||
Arrays.asList("L", "M", "R")),
|
||||
customTask("RIGHT-TO-LEFT", "R", "L", 2, 0,
|
||||
Arrays.asList("R", "M", "L")));
|
||||
List<VehicleSnapshot> vehicles = Arrays.asList(
|
||||
new VehicleSnapshot("1", 1, "L", 5, 1, 100, true, true, false),
|
||||
new VehicleSnapshot("2", 1, "R", 5, 1, 100, true, true, false));
|
||||
return new HistoricalSimulationInput(tasks, Collections.emptyList(), grid,
|
||||
traffic, vehicles);
|
||||
}
|
||||
|
||||
/** 构造活动任务驶向 Z、另一任务需要把 R 的占车清到 Y 的准入冲突。 */
|
||||
private HistoricalSimulationInput clearanceAgainstCommittedTerminalInput() {
|
||||
GridMap grid = new GridMap();
|
||||
for (String node : Arrays.asList("L", "M", "R", "Y", "Z")) {
|
||||
grid.addSite(new Site(node, 1, node.charAt(0), 1, "TEST"));
|
||||
}
|
||||
grid.connect("L", "M");
|
||||
grid.connect("M", "R");
|
||||
grid.connect("M", "Y");
|
||||
grid.connect("M", "Z");
|
||||
LinkedHashSet<String> nodes = new LinkedHashSet<String>(
|
||||
Arrays.asList("L", "M", "R", "Y", "Z"));
|
||||
LinkedHashSet<TrafficMapCompiler.EdgeDefinition> edges =
|
||||
new LinkedHashSet<TrafficMapCompiler.EdgeDefinition>();
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("L", "M"));
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("M", "R"));
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("M", "Y"));
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("M", "Z"));
|
||||
EffectiveTrafficMap traffic = new TrafficMapCompiler().compile(
|
||||
new TrafficMapCompiler.MapDefinition("COMMITTED-CLEARANCE", 1L,
|
||||
"TEST", true, "test", "test", nodes, edges,
|
||||
Collections.emptyMap(), Collections.emptySet(),
|
||||
Collections.emptySet()));
|
||||
List<HistoricalSimulationTask> tasks = Arrays.asList(
|
||||
customTask("STAGE", "Y", "Z", 0, 0,
|
||||
Arrays.asList("Y", "M", "Z")),
|
||||
customTask("SWAP", "L", "R", 1, 0,
|
||||
Arrays.asList("L", "M", "R")));
|
||||
List<VehicleSnapshot> vehicles = Arrays.asList(
|
||||
new VehicleSnapshot("1", 1, "L", 5, 1, 100, true, true, false),
|
||||
new VehicleSnapshot("2", 1, "R", 5, 1, 100, true, true, false),
|
||||
new VehicleSnapshot("3", 1, "Y", 5, 1, 100, true, true, false));
|
||||
return new HistoricalSimulationInput(tasks, Collections.emptyList(), grid,
|
||||
traffic, vehicles);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证“opposite corridor input”场景,确保相关业务规则和断言成立。
|
||||
*
|
||||
@@ -422,6 +753,147 @@ class HistoricalDispatchSimulatorTest {
|
||||
"1", 1, "A", 5, 1, 100, true, true, false)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造两个完全独立的楼层路网;唯一车辆只能通过显式换层端口到达二层任务源点。
|
||||
*/
|
||||
private HistoricalSimulationInput crossFloorInput(boolean configureTransferPorts) {
|
||||
GridMap grid = new GridMap();
|
||||
grid.addSite(new Site("V1", 1, 0, 0, "TEST"));
|
||||
grid.addSite(new Site("LIFT-1", 1, 1, 0, "TRANSFER"));
|
||||
grid.addSite(new Site("LIFT-2", 2, 1, 0, "TRANSFER"));
|
||||
grid.addSite(new Site("A", 2, 2, 0, "TEST"));
|
||||
grid.addSite(new Site("B", 2, 3, 0, "TEST"));
|
||||
grid.connect("V1", "LIFT-1");
|
||||
grid.connect("LIFT-2", "A");
|
||||
grid.connect("A", "B");
|
||||
|
||||
LinkedHashSet<String> nodes = new LinkedHashSet<String>(
|
||||
Arrays.asList("V1", "LIFT-1", "LIFT-2", "A", "B"));
|
||||
LinkedHashSet<TrafficMapCompiler.EdgeDefinition> edges =
|
||||
new LinkedHashSet<TrafficMapCompiler.EdgeDefinition>();
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("V1", "LIFT-1"));
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("LIFT-2", "A"));
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("A", "B"));
|
||||
EffectiveTrafficMap traffic = new TrafficMapCompiler().compile(
|
||||
new TrafficMapCompiler.MapDefinition("CROSS-FLOOR", 1L, "TEST", true,
|
||||
"test", "test", nodes, edges, Collections.emptyMap(),
|
||||
Collections.emptySet(), Collections.emptySet()));
|
||||
Map<Integer, String> ports = new LinkedHashMap<Integer, String>();
|
||||
if (configureTransferPorts) {
|
||||
ports.put(1, "LIFT-1");
|
||||
ports.put(2, "LIFT-2");
|
||||
}
|
||||
HistoricalSimulationTask task = customTask(
|
||||
"CROSS", "A", "B", 1, 0, Arrays.asList("A", "B"));
|
||||
return new HistoricalSimulationInput(Collections.singletonList(task),
|
||||
Collections.emptyList(), grid, traffic,
|
||||
Collections.singletonList(new VehicleSnapshot(
|
||||
"1", 1, "V1", 5, 1, 100, true, true, false)),
|
||||
new FloorTransferTopology(ports));
|
||||
}
|
||||
|
||||
/** 两个同层父任务共用唯一跨层车辆,用于证明第一个换层期间车辆不会被重复绑定。 */
|
||||
private HistoricalSimulationInput twoCrossFloorTasksInput() {
|
||||
HistoricalSimulationInput base = crossFloorInput(true);
|
||||
List<HistoricalSimulationTask> tasks = Arrays.asList(
|
||||
base.getTasks().get(0),
|
||||
customTask("CROSS-2", "A", "B", 2, 0,
|
||||
Arrays.asList("A", "B")));
|
||||
return new HistoricalSimulationInput(tasks, Collections.emptyList(),
|
||||
base.getGridMap(), base.getTrafficMap(), base.getInitialVehicles(),
|
||||
base.getFloorTransferTopology());
|
||||
}
|
||||
|
||||
/**
|
||||
* 一层车辆计划进入二层 LIFT-2,同时二层直接任务正沿 X-A-LIFT-2 驶向该出口。
|
||||
*/
|
||||
private HistoricalSimulationInput committedTransferExitInput() {
|
||||
GridMap grid = new GridMap();
|
||||
grid.addSite(new Site("V1", 1, 0, 0, "TEST"));
|
||||
grid.addSite(new Site("LIFT-1", 1, 1, 0, "TRANSFER"));
|
||||
grid.addSite(new Site("LIFT-2", 2, 0, 0, "TRANSFER"));
|
||||
grid.addSite(new Site("A", 2, 1, 0, "TEST"));
|
||||
grid.addSite(new Site("X", 2, 2, 0, "TEST"));
|
||||
grid.addSite(new Site("Y", 2, 0, 1, "TEST"));
|
||||
grid.connect("V1", "LIFT-1");
|
||||
grid.connect("LIFT-2", "A");
|
||||
grid.connect("A", "X");
|
||||
grid.connect("LIFT-2", "Y");
|
||||
|
||||
LinkedHashSet<String> nodes = new LinkedHashSet<String>(
|
||||
Arrays.asList("V1", "LIFT-1", "LIFT-2", "A", "X", "Y"));
|
||||
LinkedHashSet<TrafficMapCompiler.EdgeDefinition> edges =
|
||||
new LinkedHashSet<TrafficMapCompiler.EdgeDefinition>();
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("V1", "LIFT-1"));
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("LIFT-2", "A"));
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("A", "X"));
|
||||
edges.add(new TrafficMapCompiler.EdgeDefinition("LIFT-2", "Y"));
|
||||
EffectiveTrafficMap traffic = new TrafficMapCompiler().compile(
|
||||
new TrafficMapCompiler.MapDefinition("TRANSFER-EXIT", 1L, "TEST", true,
|
||||
"test", "test", nodes, edges, Collections.emptyMap(),
|
||||
Collections.emptySet(), Collections.emptySet()));
|
||||
List<HistoricalSimulationTask> tasks = Arrays.asList(
|
||||
customTask("DIRECT-FIRST", "X", "Y", 1, 0,
|
||||
Arrays.asList("X", "A", "LIFT-2", "Y")),
|
||||
customTask("TRANSFER-SECOND", "A", "X", 2, 0,
|
||||
Arrays.asList("A", "X")));
|
||||
List<VehicleSnapshot> vehicles = Arrays.asList(
|
||||
new VehicleSnapshot("1", 2, "X", 5, 1, 100, true, true, false),
|
||||
new VehicleSnapshot("2", 1, "V1", 5, 1, 100, true, true, false));
|
||||
Map<Integer, String> ports = new LinkedHashMap<Integer, String>();
|
||||
ports.put(1, "LIFT-1");
|
||||
ports.put(2, "LIFT-2");
|
||||
return new HistoricalSimulationInput(tasks, Collections.emptyList(), grid,
|
||||
traffic, vehicles, new FloorTransferTopology(ports));
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试用项目适配器允许不同作业并发,确保“同车不能双绑”由中心状态机保证,而不是依赖单提升机忙碌门禁。
|
||||
*/
|
||||
private static final class ParallelFloorTransferGateway
|
||||
implements FloorTransferGateway {
|
||||
private final Map<String, TransferSnapshot> snapshots =
|
||||
new LinkedHashMap<String, TransferSnapshot>();
|
||||
|
||||
@Override
|
||||
public Submission submit(FloorTransferJob job) {
|
||||
TransferSnapshot existing = snapshots.get(job.getIdempotencyKey());
|
||||
if (existing != null) {
|
||||
return new Submission(SubmissionCode.EXISTING, job, "IDEMPOTENT_REPLAY");
|
||||
}
|
||||
snapshots.put(job.getIdempotencyKey(), new TransferSnapshot(job,
|
||||
FloorTransferStatus.SUBMITTED, null, 0, "SUBMITTED"));
|
||||
return new Submission(SubmissionCode.CREATED, job, "SUBMITTED");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransferSnapshot advance(String idempotencyKey) {
|
||||
TransferSnapshot current = snapshot(idempotencyKey);
|
||||
FloorTransferJob job = current.getJob();
|
||||
TransferSnapshot next;
|
||||
if (current.getStatus() == FloorTransferStatus.SUBMITTED) {
|
||||
next = new TransferSnapshot(job, FloorTransferStatus.RUNNING,
|
||||
null, 0, "DEVICE_ACCEPTED");
|
||||
} else if (current.getStatus() == FloorTransferStatus.RUNNING) {
|
||||
next = new TransferSnapshot(job, FloorTransferStatus.COMPLETED,
|
||||
job.getTargetSite(), job.getTargetFloor(), "DEVICE_COMPLETED");
|
||||
} else {
|
||||
next = current;
|
||||
}
|
||||
snapshots.put(idempotencyKey, next);
|
||||
return next;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransferSnapshot snapshot(String idempotencyKey) {
|
||||
TransferSnapshot snapshot = snapshots.get(idempotencyKey);
|
||||
if (snapshot == null) throw new IllegalArgumentException("unknown transfer job");
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private int submittedJobCount() { return snapshots.size(); }
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证“blocked three vehicle corridor input”场景,确保相关业务规则和断言成立。
|
||||
*
|
||||
|
||||
@@ -75,6 +75,10 @@ class HistoricalSimulationInputFactoryTest {
|
||||
assertTrue(input.getTrafficMap().getEdges().size() > 1000);
|
||||
assertEquals(Arrays.asList("1", "2", "3"), input.getInitialVehicles().stream()
|
||||
.map(vehicle -> vehicle.getVehicleId()).collect(Collectors.toList()));
|
||||
assertEquals("070501", input.getFloorTransferTopology().portFor(1).get());
|
||||
assertEquals("070502", input.getFloorTransferTopology().portFor(2).get());
|
||||
assertEquals("070503", input.getFloorTransferTopology().portFor(3).get());
|
||||
assertFalse(input.getGridMap().getNeighbors("070501").contains("070502"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user