feat(javaTest): rank routes by live traffic quality
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ft546.wcs.demo.traffic;
|
||||||
|
|
||||||
|
/** 把一条地图候选与同一交通快照下算出的质量证据绑定。 */
|
||||||
|
public final class RouteCandidateEvaluation {
|
||||||
|
private final RouteCandidate candidate;
|
||||||
|
private final RouteQuality quality;
|
||||||
|
|
||||||
|
RouteCandidateEvaluation(RouteCandidate candidate, RouteQuality quality) {
|
||||||
|
this.candidate = candidate;
|
||||||
|
this.quality = quality;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 返回被评价的候选路线。 */
|
||||||
|
public RouteCandidate getCandidate() { return candidate; }
|
||||||
|
|
||||||
|
/** 返回该候选的完整业务质量元组。 */
|
||||||
|
public RouteQuality getQuality() { return quality; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.ft546.wcs.demo.traffic;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 一次接近段或服务段规划的最终选择及全部审计证据。 */
|
||||||
|
public final class RoutePlanDecision {
|
||||||
|
private final RouteCandidateSet candidateSet;
|
||||||
|
private final List<RouteCandidateEvaluation> evaluations;
|
||||||
|
private final RouteCandidateEvaluation selected;
|
||||||
|
private final String decisionReason;
|
||||||
|
|
||||||
|
RoutePlanDecision(RouteCandidateSet candidateSet,
|
||||||
|
List<RouteCandidateEvaluation> evaluations,
|
||||||
|
RouteCandidateEvaluation selected, String decisionReason) {
|
||||||
|
this.candidateSet = candidateSet;
|
||||||
|
this.evaluations = Collections.unmodifiableList(
|
||||||
|
new ArrayList<RouteCandidateEvaluation>(evaluations));
|
||||||
|
this.selected = selected;
|
||||||
|
this.decisionReason = decisionReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RouteCandidateSet getCandidateSet() { return candidateSet; }
|
||||||
|
public List<RouteCandidateEvaluation> getEvaluations() { return evaluations; }
|
||||||
|
public RouteCandidateEvaluation getSelected() { return selected; }
|
||||||
|
public List<String> getSelectedPath() { return selected.getCandidate().getPath(); }
|
||||||
|
public RouteQuality getSelectedQuality() { return selected.getQuality(); }
|
||||||
|
public String getDecisionReason() { return decisionReason; }
|
||||||
|
|
||||||
|
/** 返回选中距离相对物理最短距离的倍率,1.0 表示没有绕行。 */
|
||||||
|
public double getDetourRatio() {
|
||||||
|
double shortest = candidateSet.getShortestPhysicalDistance();
|
||||||
|
return shortest == 0.0d ? 1.0d
|
||||||
|
: selected.getCandidate().getPhysicalDistance() / shortest;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.ft546.wcs.demo.traffic;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 一条候选路线按固定业务顺序组成的可解释比较元组。 */
|
||||||
|
public final class RouteQuality implements Comparable<RouteQuality> {
|
||||||
|
private final int oppositeDirectionConflictCount;
|
||||||
|
private final int occupiedNodeCount;
|
||||||
|
private final int incompatibleReservedEdgeCount;
|
||||||
|
private final int activeRouteOverlapEdgeCount;
|
||||||
|
private final int turnCount;
|
||||||
|
private final double physicalDistance;
|
||||||
|
private final List<String> canonicalPath;
|
||||||
|
|
||||||
|
RouteQuality(int oppositeDirectionConflictCount, int occupiedNodeCount,
|
||||||
|
int incompatibleReservedEdgeCount, int activeRouteOverlapEdgeCount,
|
||||||
|
int turnCount, double physicalDistance, List<String> canonicalPath) {
|
||||||
|
this.oppositeDirectionConflictCount = oppositeDirectionConflictCount;
|
||||||
|
this.occupiedNodeCount = occupiedNodeCount;
|
||||||
|
this.incompatibleReservedEdgeCount = incompatibleReservedEdgeCount;
|
||||||
|
this.activeRouteOverlapEdgeCount = activeRouteOverlapEdgeCount;
|
||||||
|
this.turnCount = turnCount;
|
||||||
|
this.physicalDistance = physicalDistance;
|
||||||
|
this.canonicalPath = Collections.unmodifiableList(
|
||||||
|
new ArrayList<String>(canonicalPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOppositeDirectionConflictCount() { return oppositeDirectionConflictCount; }
|
||||||
|
public int getOccupiedNodeCount() { return occupiedNodeCount; }
|
||||||
|
public int getIncompatibleReservedEdgeCount() { return incompatibleReservedEdgeCount; }
|
||||||
|
public int getActiveRouteOverlapEdgeCount() { return activeRouteOverlapEdgeCount; }
|
||||||
|
public int getTurnCount() { return turnCount; }
|
||||||
|
public double getPhysicalDistance() { return physicalDistance; }
|
||||||
|
public List<String> getCanonicalPath() { return canonicalPath; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按中心调度确认的硬顺序逐项比较,不使用会掩盖高优先级风险的加权总分。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int compareTo(RouteQuality other) {
|
||||||
|
int result = Integer.compare(oppositeDirectionConflictCount,
|
||||||
|
other.oppositeDirectionConflictCount);
|
||||||
|
if (result != 0) return result;
|
||||||
|
result = Integer.compare(occupiedNodeCount, other.occupiedNodeCount);
|
||||||
|
if (result != 0) return result;
|
||||||
|
result = Integer.compare(incompatibleReservedEdgeCount,
|
||||||
|
other.incompatibleReservedEdgeCount);
|
||||||
|
if (result != 0) return result;
|
||||||
|
result = Integer.compare(activeRouteOverlapEdgeCount,
|
||||||
|
other.activeRouteOverlapEdgeCount);
|
||||||
|
if (result != 0) return result;
|
||||||
|
result = Integer.compare(turnCount, other.turnCount);
|
||||||
|
if (result != 0) return result;
|
||||||
|
result = Double.compare(physicalDistance, other.physicalDistance);
|
||||||
|
if (result != 0) return result;
|
||||||
|
return comparePaths(canonicalPath, other.canonicalPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 以完整站点序列作最终稳定决胜,确保重复仿真得到同一路线。 */
|
||||||
|
private int comparePaths(List<String> left, List<String> right) {
|
||||||
|
int common = Math.min(left.size(), right.size());
|
||||||
|
for (int index = 0; index < common; index++) {
|
||||||
|
int result = left.get(index).compareTo(right.get(index));
|
||||||
|
if (result != 0) return result;
|
||||||
|
}
|
||||||
|
return Integer.compare(left.size(), right.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package com.ft546.wcs.demo.traffic;
|
||||||
|
|
||||||
|
import com.ft546.wcs.demo.model.Site;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** 使用同一个实时交通快照为有界候选排序并选择业务质量最优路线。 */
|
||||||
|
public final class RouteQualityEvaluator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评价全部候选并按固定质量元组选择第一名。
|
||||||
|
*
|
||||||
|
* @param map 权威站点坐标
|
||||||
|
* @param candidates 已通过物理距离边界的候选集合
|
||||||
|
* @param snapshot 同一 tick 冻结的交通证据
|
||||||
|
* @param taskId 当前任务,用于排除自己已持有的资源
|
||||||
|
* @param vehicleId 当前车辆,用于排除自己的起点占用
|
||||||
|
* @return 包含所有候选质量和最终业务原因的决定
|
||||||
|
*/
|
||||||
|
public RoutePlanDecision select(GridMap map, RouteCandidateSet candidates,
|
||||||
|
TrafficRouteSnapshot snapshot,
|
||||||
|
String taskId, String vehicleId) {
|
||||||
|
if (candidates == null || candidates.getCandidates().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("at least one route candidate is required");
|
||||||
|
}
|
||||||
|
List<RouteCandidateEvaluation> evaluations =
|
||||||
|
new ArrayList<RouteCandidateEvaluation>();
|
||||||
|
RouteCandidateEvaluation baseline = null;
|
||||||
|
for (RouteCandidate candidate : candidates.getCandidates()) {
|
||||||
|
RouteCandidateEvaluation evaluation = new RouteCandidateEvaluation(candidate,
|
||||||
|
evaluate(map, candidate, snapshot, taskId, vehicleId));
|
||||||
|
evaluations.add(evaluation);
|
||||||
|
if (candidate.getPath().equals(candidates.getShortestPath())) baseline = evaluation;
|
||||||
|
}
|
||||||
|
Collections.sort(evaluations, Comparator.comparing(
|
||||||
|
RouteCandidateEvaluation::getQuality));
|
||||||
|
RouteCandidateEvaluation selected = evaluations.get(0);
|
||||||
|
if (baseline == null) baseline = selected;
|
||||||
|
return new RoutePlanDecision(candidates, evaluations, selected,
|
||||||
|
decisionReason(selected, baseline));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算单条候选的交通风险、操作复杂度和物理距离元组。
|
||||||
|
*/
|
||||||
|
public RouteQuality evaluate(GridMap map, RouteCandidate candidate,
|
||||||
|
TrafficRouteSnapshot snapshot,
|
||||||
|
String taskId, String vehicleId) {
|
||||||
|
if (map == null || candidate == null || snapshot == null
|
||||||
|
|| taskId == null || vehicleId == null) {
|
||||||
|
throw new IllegalArgumentException("map, candidate, snapshot and owners are required");
|
||||||
|
}
|
||||||
|
List<String> path = candidate.getPath();
|
||||||
|
int opposite = 0;
|
||||||
|
int occupied = 0;
|
||||||
|
int incompatible = 0;
|
||||||
|
int overlap = 0;
|
||||||
|
|
||||||
|
// 路径下标 0 是当前车辆所在点,只有未来节点被其他车辆占用才形成候选风险。
|
||||||
|
for (int index = 1; index < path.size(); index++) {
|
||||||
|
String from = path.get(index - 1);
|
||||||
|
String to = path.get(index);
|
||||||
|
if (snapshot.isOccupiedByOther(to, vehicleId)) occupied++;
|
||||||
|
boolean oppositeOnEdge = snapshot.hasOppositeDirectionOwnedByOther(
|
||||||
|
from, to, taskId);
|
||||||
|
if (oppositeOnEdge) opposite++;
|
||||||
|
// 同一物理边已计为对向风险时不在“不兼容边预约”中重复处罚。
|
||||||
|
if (!oppositeOnEdge
|
||||||
|
&& snapshot.isExclusiveEdgeOwnedByOther(from, to, taskId)) {
|
||||||
|
incompatible++;
|
||||||
|
}
|
||||||
|
if (snapshot.overlapsOtherActiveRoute(from, to, taskId)) overlap++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RouteQuality(opposite, occupied, incompatible, overlap,
|
||||||
|
countTurns(map, path), candidate.getPhysicalDistance(), path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 计算连续边方向向量改变次数;同一直线上的不同边长不算转弯。 */
|
||||||
|
private int countTurns(GridMap map, List<String> path) {
|
||||||
|
int turns = 0;
|
||||||
|
for (int index = 2; index < path.size(); index++) {
|
||||||
|
Site first = map.getSite(path.get(index - 2));
|
||||||
|
Site middle = map.getSite(path.get(index - 1));
|
||||||
|
Site last = map.getSite(path.get(index));
|
||||||
|
if (first == null || middle == null || last == null) {
|
||||||
|
throw new IllegalArgumentException("candidate references an unknown site");
|
||||||
|
}
|
||||||
|
if (first.getFloor() != middle.getFloor()
|
||||||
|
|| middle.getFloor() != last.getFloor()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int firstX = normalize(middle.getX() - first.getX());
|
||||||
|
int firstY = normalize(middle.getY() - first.getY());
|
||||||
|
int secondX = normalize(last.getX() - middle.getX());
|
||||||
|
int secondY = normalize(last.getY() - middle.getY());
|
||||||
|
if (firstX != secondX || firstY != secondY) turns++;
|
||||||
|
}
|
||||||
|
return turns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前地图为正交网格,方向只需要保留坐标增量的符号。 */
|
||||||
|
private int normalize(int value) {
|
||||||
|
return Integer.compare(value, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解释选中路线相对物理最短路线最先胜出的业务指标。 */
|
||||||
|
private String decisionReason(RouteCandidateEvaluation selected,
|
||||||
|
RouteCandidateEvaluation baseline) {
|
||||||
|
if (selected.getCandidate().getPath().equals(baseline.getCandidate().getPath())) {
|
||||||
|
return "SHORTEST_AND_BEST";
|
||||||
|
}
|
||||||
|
RouteQuality winner = selected.getQuality();
|
||||||
|
RouteQuality shortest = baseline.getQuality();
|
||||||
|
if (winner.getOppositeDirectionConflictCount()
|
||||||
|
< shortest.getOppositeDirectionConflictCount()) {
|
||||||
|
return "FEWER_OPPOSITE_DIRECTION_CONFLICTS";
|
||||||
|
}
|
||||||
|
if (winner.getOccupiedNodeCount() < shortest.getOccupiedNodeCount()) {
|
||||||
|
return "FEWER_OCCUPIED_NODES";
|
||||||
|
}
|
||||||
|
if (winner.getIncompatibleReservedEdgeCount()
|
||||||
|
< shortest.getIncompatibleReservedEdgeCount()) {
|
||||||
|
return "FEWER_INCOMPATIBLE_RESERVED_EDGES";
|
||||||
|
}
|
||||||
|
if (winner.getActiveRouteOverlapEdgeCount()
|
||||||
|
< shortest.getActiveRouteOverlapEdgeCount()) {
|
||||||
|
return "FEWER_ACTIVE_ROUTE_OVERLAPS";
|
||||||
|
}
|
||||||
|
if (winner.getTurnCount() < shortest.getTurnCount()) return "FEWER_TURNS";
|
||||||
|
if (winner.getPhysicalDistance() < shortest.getPhysicalDistance()) {
|
||||||
|
return "SHORTER_PHYSICAL_DISTANCE";
|
||||||
|
}
|
||||||
|
return "CANONICAL_PATH_TIE_BREAK";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
package com.ft546.wcs.demo.traffic;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存一次路线比较所使用的不可变实时交通证据。
|
||||||
|
*
|
||||||
|
* <p>同一候选集合必须使用同一快照,避免前一条路线和后一条路线读取到不同 tick 的占用状态。
|
||||||
|
*/
|
||||||
|
public final class TrafficRouteSnapshot {
|
||||||
|
private final Map<String, Set<String>> occupiedNodes;
|
||||||
|
private final Map<String, Set<String>> exclusiveEdges;
|
||||||
|
private final Map<String, Set<String>> directedReservations;
|
||||||
|
private final Map<String, List<String>> activeRoutes;
|
||||||
|
|
||||||
|
private TrafficRouteSnapshot(Builder builder) {
|
||||||
|
this.occupiedNodes = immutableSets(builder.occupiedNodes);
|
||||||
|
this.exclusiveEdges = immutableSets(builder.exclusiveEdges);
|
||||||
|
this.directedReservations = immutableSets(builder.directedReservations);
|
||||||
|
Map<String, List<String>> routes = new LinkedHashMap<String, List<String>>();
|
||||||
|
for (Map.Entry<String, List<String>> entry : builder.activeRoutes.entrySet()) {
|
||||||
|
routes.put(entry.getKey(), Collections.unmodifiableList(
|
||||||
|
new ArrayList<String>(entry.getValue())));
|
||||||
|
}
|
||||||
|
this.activeRoutes = Collections.unmodifiableMap(routes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建快照构造器,调用方可从车辆、资源台账和走廊预约逐项填充证据。 */
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 返回没有任何临时交通风险的空快照,适用于纯地图规划和单元测试。 */
|
||||||
|
public static TrafficRouteSnapshot empty() {
|
||||||
|
return builder().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断候选未来节点是否由其他车辆占用;当前车辆自己的节点不形成风险。 */
|
||||||
|
boolean isOccupiedByOther(String siteId, String vehicleId) {
|
||||||
|
return containsOther(occupiedNodes.get(siteId), vehicleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断物理边是否由其他任务独占;当前任务已经合法持有的边可以继续使用。 */
|
||||||
|
boolean isExclusiveEdgeOwnedByOther(String from, String to, String taskId) {
|
||||||
|
return containsOther(exclusiveEdges.get(canonicalEdge(from, to)), taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断同一走廊是否已经被其他任务预约为候选移动的相反方向。 */
|
||||||
|
boolean hasOppositeDirectionOwnedByOther(String from, String to, String taskId) {
|
||||||
|
return containsOther(directedReservations.get(directedEdge(to, from)), taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断候选物理边是否与其他活动任务尚未执行的路线重合。 */
|
||||||
|
boolean overlapsOtherActiveRoute(String from, String to, String taskId) {
|
||||||
|
String edge = canonicalEdge(from, to);
|
||||||
|
for (Map.Entry<String, List<String>> entry : activeRoutes.entrySet()) {
|
||||||
|
if (entry.getKey().equals(taskId)) continue;
|
||||||
|
List<String> route = entry.getValue();
|
||||||
|
for (int index = 1; index < route.size(); index++) {
|
||||||
|
if (edge.equals(canonicalEdge(route.get(index - 1), route.get(index)))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 返回集合中是否至少有一个非当前业务所有者。 */
|
||||||
|
private boolean containsOther(Set<String> owners, String currentOwner) {
|
||||||
|
if (owners == null) return false;
|
||||||
|
for (String owner : owners) {
|
||||||
|
if (!owner.equals(currentOwner)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将内部可变集合复制为不可变证据,后续资源变化不会污染本次决策。 */
|
||||||
|
private static Map<String, Set<String>> immutableSets(Map<String, Set<String>> source) {
|
||||||
|
Map<String, Set<String>> result = new LinkedHashMap<String, Set<String>>();
|
||||||
|
for (Map.Entry<String, Set<String>> entry : source.entrySet()) {
|
||||||
|
result.put(entry.getKey(), Collections.unmodifiableSet(
|
||||||
|
new LinkedHashSet<String>(entry.getValue())));
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableMap(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 把无向资源边规范为相同键,使 A-B 与 B-A 指向同一物理边。 */
|
||||||
|
static String canonicalEdge(String left, String right) {
|
||||||
|
return left.compareTo(right) <= 0 ? left + "\u0000" + right : right + "\u0000" + left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保留方向生成走廊预约键,使 A→B 与 B→A 可以分别判断。 */
|
||||||
|
static String directedEdge(String from, String to) {
|
||||||
|
return from + "\u0000" + to;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 在决策时收集当前 tick 交通状态的可变构造器。 */
|
||||||
|
public static final class Builder {
|
||||||
|
private final Map<String, Set<String>> occupiedNodes =
|
||||||
|
new LinkedHashMap<String, Set<String>>();
|
||||||
|
private final Map<String, Set<String>> exclusiveEdges =
|
||||||
|
new LinkedHashMap<String, Set<String>>();
|
||||||
|
private final Map<String, Set<String>> directedReservations =
|
||||||
|
new LinkedHashMap<String, Set<String>>();
|
||||||
|
private final Map<String, List<String>> activeRoutes =
|
||||||
|
new LinkedHashMap<String, List<String>>();
|
||||||
|
|
||||||
|
/** 登记车辆当前实际占据的节点。 */
|
||||||
|
public Builder occupyNode(String siteId, String vehicleId) {
|
||||||
|
add(occupiedNodes, siteId, vehicleId);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 登记资源台账中不能与其他任务同时持有的物理边。 */
|
||||||
|
public Builder reserveEdge(String from, String to, String taskId) {
|
||||||
|
add(exclusiveEdges, canonicalEdge(from, to), taskId);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 登记走廊从 from 到 to 的方向预约。 */
|
||||||
|
public Builder reserveDirection(String from, String to, String taskId) {
|
||||||
|
add(directedReservations, directedEdge(from, to), taskId);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 登记其他活动任务从当前位置开始尚未走完的节点序列。 */
|
||||||
|
public Builder activeRoute(String taskId, List<String> remainingRoute) {
|
||||||
|
requireText(taskId, "task id");
|
||||||
|
if (remainingRoute == null || remainingRoute.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("active route is required");
|
||||||
|
}
|
||||||
|
activeRoutes.put(taskId, new ArrayList<String>(remainingRoute));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 冻结本轮交通证据,后续候选评价只读该对象。 */
|
||||||
|
public TrafficRouteSnapshot build() {
|
||||||
|
return new TrafficRouteSnapshot(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 向同一资源追加合法所有者,支持同向走廊存在多个任务预约。 */
|
||||||
|
private void add(Map<String, Set<String>> target, String resource, String owner) {
|
||||||
|
requireText(resource, "resource");
|
||||||
|
requireText(owner, "owner");
|
||||||
|
Set<String> owners = target.get(resource);
|
||||||
|
if (owners == null) {
|
||||||
|
owners = new LinkedHashSet<String>();
|
||||||
|
target.put(resource, owners);
|
||||||
|
}
|
||||||
|
owners.add(owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拒绝空业务标识,避免快照中出现无法解释的匿名资源。 */
|
||||||
|
private void requireText(String value, String name) {
|
||||||
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException(name + " must not be blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.ft546.wcs.demo.traffic;
|
||||||
|
|
||||||
|
import com.ft546.wcs.demo.model.Site;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
class RouteQualityEvaluatorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void countsLiveTrafficRiskWithoutChargingResourcesOwnedByCurrentTaskOrVehicle() {
|
||||||
|
GridMap map = map();
|
||||||
|
RouteCandidate candidate = new RouteCandidate(Arrays.asList("A", "B", "D"), 2.0d);
|
||||||
|
TrafficRouteSnapshot snapshot = TrafficRouteSnapshot.builder()
|
||||||
|
.occupyNode("A", "AGV-1")
|
||||||
|
.occupyNode("B", "AGV-2")
|
||||||
|
.reserveDirection("D", "B", "TASK-2")
|
||||||
|
.reserveEdge("B", "D", "TASK-2")
|
||||||
|
.activeRoute("TASK-2", Arrays.asList("D", "B"))
|
||||||
|
.reserveEdge("A", "B", "TASK-1")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
RouteQuality quality = new RouteQualityEvaluator().evaluate(
|
||||||
|
map, candidate, snapshot, "TASK-1", "AGV-1");
|
||||||
|
|
||||||
|
assertEquals(1, quality.getOppositeDirectionConflictCount());
|
||||||
|
assertEquals(1, quality.getOccupiedNodeCount());
|
||||||
|
assertEquals(0, quality.getIncompatibleReservedEdgeCount());
|
||||||
|
assertEquals(1, quality.getActiveRouteOverlapEdgeCount());
|
||||||
|
assertEquals(0, quality.getTurnCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void lexicographicOrderPrefersConflictAvoidanceBeforeDistance() {
|
||||||
|
GridMap map = map();
|
||||||
|
RouteCandidate shortestWithOppositeConflict =
|
||||||
|
new RouteCandidate(Arrays.asList("A", "B", "D"), 2.0d);
|
||||||
|
RouteCandidate longerWithoutConflict =
|
||||||
|
new RouteCandidate(Arrays.asList("A", "C", "D"), 2.8d);
|
||||||
|
RouteCandidateSet candidates = new RouteCandidateSet(
|
||||||
|
shortestWithOppositeConflict.getPath(), 2.0d,
|
||||||
|
Arrays.asList(shortestWithOppositeConflict, longerWithoutConflict), false);
|
||||||
|
TrafficRouteSnapshot snapshot = TrafficRouteSnapshot.builder()
|
||||||
|
.reserveDirection("D", "B", "OTHER")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
RoutePlanDecision decision = new RouteQualityEvaluator().select(
|
||||||
|
map, candidates, snapshot, "TASK-1", "AGV-1");
|
||||||
|
|
||||||
|
assertEquals(Arrays.asList("A", "C", "D"), decision.getSelectedPath());
|
||||||
|
assertEquals("FEWER_OPPOSITE_DIRECTION_CONFLICTS", decision.getDecisionReason());
|
||||||
|
assertEquals(2, decision.getEvaluations().size());
|
||||||
|
assertEquals(1.4d, decision.getDetourRatio(), 0.000001d);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void lowerTupleFieldsDecideOnlyWhenAllHigherFieldsAreEqual() {
|
||||||
|
GridMap map = map();
|
||||||
|
RouteCandidate occupied = new RouteCandidate(Arrays.asList("A", "B", "D"), 2.0d);
|
||||||
|
RouteCandidate reserved = new RouteCandidate(Arrays.asList("A", "C", "D"), 2.8d);
|
||||||
|
TrafficRouteSnapshot snapshot = TrafficRouteSnapshot.builder()
|
||||||
|
.occupyNode("B", "OTHER-AGV")
|
||||||
|
.reserveEdge("A", "C", "OTHER-TASK")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
RoutePlanDecision occupiedLoses = new RouteQualityEvaluator().select(
|
||||||
|
map, new RouteCandidateSet(occupied.getPath(), 2.0d,
|
||||||
|
Arrays.asList(occupied, reserved), false),
|
||||||
|
snapshot, "TASK-1", "AGV-1");
|
||||||
|
assertEquals(reserved.getPath(), occupiedLoses.getSelectedPath());
|
||||||
|
|
||||||
|
RouteCandidate canonicalFirst = new RouteCandidate(
|
||||||
|
Arrays.asList("A", "B", "D"), 2.0d);
|
||||||
|
RouteCandidate canonicalSecond = new RouteCandidate(
|
||||||
|
Arrays.asList("A", "E", "D"), 2.0d);
|
||||||
|
RoutePlanDecision canonicalDecision = new RouteQualityEvaluator().select(
|
||||||
|
map, new RouteCandidateSet(canonicalFirst.getPath(), 2.0d,
|
||||||
|
Arrays.asList(canonicalSecond, canonicalFirst), false),
|
||||||
|
TrafficRouteSnapshot.empty(), "TASK-1", "AGV-1");
|
||||||
|
assertEquals(canonicalFirst.getPath(), canonicalDecision.getSelectedPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void countsTurnsUsingDirectionChangesAndActiveOverlapAsUndirectedEdges() {
|
||||||
|
GridMap map = map();
|
||||||
|
RouteCandidate turning = new RouteCandidate(Arrays.asList("A", "B", "F"), 2.0d);
|
||||||
|
TrafficRouteSnapshot snapshot = TrafficRouteSnapshot.builder()
|
||||||
|
.activeRoute("OTHER", Arrays.asList("F", "B"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
RouteQuality quality = new RouteQualityEvaluator().evaluate(
|
||||||
|
map, turning, snapshot, "TASK-1", "AGV-1");
|
||||||
|
|
||||||
|
assertEquals(1, quality.getTurnCount());
|
||||||
|
assertEquals(1, quality.getActiveRouteOverlapEdgeCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
private GridMap map() {
|
||||||
|
GridMap map = new GridMap();
|
||||||
|
add(map, "A", 0, 0);
|
||||||
|
add(map, "B", 1, 0);
|
||||||
|
add(map, "C", 1, 1);
|
||||||
|
add(map, "D", 2, 0);
|
||||||
|
add(map, "E", 1, -1);
|
||||||
|
add(map, "F", 1, 1);
|
||||||
|
map.connect("A", "B");
|
||||||
|
map.connect("B", "D");
|
||||||
|
map.connect("A", "C");
|
||||||
|
map.connect("C", "D");
|
||||||
|
map.connect("A", "E");
|
||||||
|
map.connect("E", "D");
|
||||||
|
map.connect("B", "F");
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void add(GridMap map, String id, int x, int y) {
|
||||||
|
map.addSite(new Site(id, 1, x, y, "MAIN"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user