fix(javaTest): validate route quality inputs
This commit is contained in:
@@ -31,11 +31,11 @@
|
|||||||
- `TrafficRouteSnapshot` 新增包内查询 `hasOppositeActiveRoute(String from, String to, String taskId)`。
|
- `TrafficRouteSnapshot` 新增包内查询 `hasOppositeActiveRoute(String from, String to, String taskId)`。
|
||||||
- `RouteQualityEvaluator.evaluate(...)` 继续返回 `RouteQuality`;非法节点、非邻接边或距离不一致时抛 `IllegalArgumentException`。
|
- `RouteQualityEvaluator.evaluate(...)` 继续返回 `RouteQuality`;非法节点、非邻接边或距离不一致时抛 `IllegalArgumentException`。
|
||||||
|
|
||||||
- [ ] **Step 1: 写失败测试**
|
- [x] **Step 1: 写失败测试**
|
||||||
|
|
||||||
增加三个测试:活动路线 `D -> B -> A` 对候选 `A -> B -> D` 计入对向冲突;候选包含未知站点或断边时被拒绝;候选声明距离与地图距离不一致时被拒绝。现有同向活动路线仍只计重合,不重复计对向。
|
增加三个测试:活动路线 `D -> B -> A` 对候选 `A -> B -> D` 计入对向冲突;候选包含未知站点或断边时被拒绝;候选声明距离与地图距离不一致时被拒绝。现有同向活动路线仍只计重合,不重复计对向。
|
||||||
|
|
||||||
- [ ] **Step 2: 运行 RED**
|
- [x] **Step 2: 运行 RED**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mvn -f javaTest/pom.xml -Dtest=RouteQualityEvaluatorTest test
|
mvn -f javaTest/pom.xml -Dtest=RouteQualityEvaluatorTest test
|
||||||
@@ -43,11 +43,11 @@ mvn -f javaTest/pom.xml -Dtest=RouteQualityEvaluatorTest test
|
|||||||
|
|
||||||
预期新增断言失败或编译失败,证明测试没有被旧实现意外满足。
|
预期新增断言失败或编译失败,证明测试没有被旧实现意外满足。
|
||||||
|
|
||||||
- [ ] **Step 3: 实现最小修复**
|
- [x] **Step 3: 实现最小修复**
|
||||||
|
|
||||||
在快照中遍历其他活动路线的有向相邻边,只有 `to -> from` 命中时返回对向;在评价入口调用 `RouteCandidatePlanner.pathDistance(map, path)` 校验路径连续性和距离,使用数值容差 `1e-6`,再执行现有质量元组计算。对已识别的对向边跳过不兼容预约重复计数。
|
在快照中遍历其他活动路线的有向相邻边,只有 `to -> from` 命中时返回对向;在评价入口调用 `RouteCandidatePlanner.pathDistance(map, path)` 校验路径连续性和距离,使用数值容差 `1e-6`,再执行现有质量元组计算。对已识别的对向边跳过不兼容预约重复计数。
|
||||||
|
|
||||||
- [ ] **Step 4: 运行 GREEN**
|
- [x] **Step 4: 运行 GREEN**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mvn -f javaTest/pom.xml -Dtest=RouteQualityEvaluatorTest,RouteCandidatePlannerTest test
|
mvn -f javaTest/pom.xml -Dtest=RouteQualityEvaluatorTest,RouteCandidatePlannerTest test
|
||||||
@@ -55,7 +55,7 @@ mvn -f javaTest/pom.xml -Dtest=RouteQualityEvaluatorTest,RouteCandidatePlannerTe
|
|||||||
|
|
||||||
预期所有路线单测通过,并保留原有确定性、110%边界和候选上限行为。
|
预期所有路线单测通过,并保留原有确定性、110%边界和候选上限行为。
|
||||||
|
|
||||||
- [ ] **Step 5: 提交**
|
- [x] **Step 5: 提交**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add javaTest/src/main/java/com/ft546/wcs/demo/traffic/TrafficRouteSnapshot.java javaTest/src/main/java/com/ft546/wcs/demo/traffic/RouteQualityEvaluator.java javaTest/src/test/java/com/ft546/wcs/demo/traffic/RouteQualityEvaluatorTest.java
|
git add javaTest/src/main/java/com/ft546/wcs/demo/traffic/TrafficRouteSnapshot.java javaTest/src/main/java/com/ft546/wcs/demo/traffic/RouteQualityEvaluator.java javaTest/src/test/java/com/ft546/wcs/demo/traffic/RouteQualityEvaluatorTest.java
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import java.util.List;
|
|||||||
|
|
||||||
/** 使用同一个实时交通快照为有界候选排序并选择业务质量最优路线。 */
|
/** 使用同一个实时交通快照为有界候选排序并选择业务质量最优路线。 */
|
||||||
public final class RouteQualityEvaluator {
|
public final class RouteQualityEvaluator {
|
||||||
|
private static final double DISTANCE_EPSILON = 0.000001d;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 评价全部候选并按固定质量元组选择第一名。
|
* 评价全部候选并按固定质量元组选择第一名。
|
||||||
@@ -54,6 +55,7 @@ public final class RouteQualityEvaluator {
|
|||||||
throw new IllegalArgumentException("map, candidate, snapshot and owners are required");
|
throw new IllegalArgumentException("map, candidate, snapshot and owners are required");
|
||||||
}
|
}
|
||||||
List<String> path = candidate.getPath();
|
List<String> path = candidate.getPath();
|
||||||
|
validateCandidatePath(map, candidate);
|
||||||
int opposite = 0;
|
int opposite = 0;
|
||||||
int occupied = 0;
|
int occupied = 0;
|
||||||
int incompatible = 0;
|
int incompatible = 0;
|
||||||
@@ -64,8 +66,8 @@ public final class RouteQualityEvaluator {
|
|||||||
String from = path.get(index - 1);
|
String from = path.get(index - 1);
|
||||||
String to = path.get(index);
|
String to = path.get(index);
|
||||||
if (snapshot.isOccupiedByOther(to, vehicleId)) occupied++;
|
if (snapshot.isOccupiedByOther(to, vehicleId)) occupied++;
|
||||||
boolean oppositeOnEdge = snapshot.hasOppositeDirectionOwnedByOther(
|
boolean oppositeOnEdge = snapshot.hasOppositeDirectionOwnedByOther(from, to, taskId)
|
||||||
from, to, taskId);
|
|| snapshot.hasOppositeActiveRoute(from, to, taskId);
|
||||||
if (oppositeOnEdge) opposite++;
|
if (oppositeOnEdge) opposite++;
|
||||||
// 同一物理边已计为对向风险时不在“不兼容边预约”中重复处罚。
|
// 同一物理边已计为对向风险时不在“不兼容边预约”中重复处罚。
|
||||||
if (!oppositeOnEdge
|
if (!oppositeOnEdge
|
||||||
@@ -79,6 +81,17 @@ public final class RouteQualityEvaluator {
|
|||||||
countTurns(map, path), candidate.getPhysicalDistance(), path);
|
countTurns(map, path), candidate.getPhysicalDistance(), path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在公开评价入口重新确认候选仍然属于权威地图,防止调用方绕过候选规划器注入非法路径。
|
||||||
|
*/
|
||||||
|
private void validateCandidatePath(GridMap map, RouteCandidate candidate) {
|
||||||
|
double actualDistance = new RouteCandidatePlanner().pathDistance(map, candidate.getPath());
|
||||||
|
if (Math.abs(actualDistance - candidate.getPhysicalDistance()) > DISTANCE_EPSILON) {
|
||||||
|
throw new IllegalArgumentException("candidate distance does not match map path: "
|
||||||
|
+ candidate.getPhysicalDistance() + " != " + actualDistance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 计算连续边方向向量改变次数;同一直线上的不同边长不算转弯。 */
|
/** 计算连续边方向向量改变次数;同一直线上的不同边长不算转弯。 */
|
||||||
private int countTurns(GridMap map, List<String> path) {
|
private int countTurns(GridMap map, List<String> path) {
|
||||||
int turns = 0;
|
int turns = 0;
|
||||||
|
|||||||
@@ -56,6 +56,23 @@ public final class TrafficRouteSnapshot {
|
|||||||
return containsOther(directedReservations.get(directedEdge(to, from)), taskId);
|
return containsOther(directedReservations.get(directedEdge(to, from)), taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断候选边是否与其他活动任务的剩余路线反向重叠;活动路线还未走完,因此也属于对向风险。
|
||||||
|
*/
|
||||||
|
boolean hasOppositeActiveRoute(String from, String to, String taskId) {
|
||||||
|
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 (directedEdge(from, to).equals(
|
||||||
|
directedEdge(route.get(index), route.get(index - 1)))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/** 判断候选物理边是否与其他活动任务尚未执行的路线重合。 */
|
/** 判断候选物理边是否与其他活动任务尚未执行的路线重合。 */
|
||||||
boolean overlapsOtherActiveRoute(String from, String to, String taskId) {
|
boolean overlapsOtherActiveRoute(String from, String to, String taskId) {
|
||||||
String edge = canonicalEdge(from, to);
|
String edge = canonicalEdge(from, to);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import java.util.Arrays;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
class RouteQualityEvaluatorTest {
|
class RouteQualityEvaluatorTest {
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ class RouteQualityEvaluatorTest {
|
|||||||
.occupyNode("B", "AGV-2")
|
.occupyNode("B", "AGV-2")
|
||||||
.reserveDirection("D", "B", "TASK-2")
|
.reserveDirection("D", "B", "TASK-2")
|
||||||
.reserveEdge("B", "D", "TASK-2")
|
.reserveEdge("B", "D", "TASK-2")
|
||||||
.activeRoute("TASK-2", Arrays.asList("D", "B"))
|
.activeRoute("TASK-2", Arrays.asList("B", "D"))
|
||||||
.reserveEdge("A", "B", "TASK-1")
|
.reserveEdge("A", "B", "TASK-1")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ class RouteQualityEvaluatorTest {
|
|||||||
RouteCandidate shortestWithOppositeConflict =
|
RouteCandidate shortestWithOppositeConflict =
|
||||||
new RouteCandidate(Arrays.asList("A", "B", "D"), 2.0d);
|
new RouteCandidate(Arrays.asList("A", "B", "D"), 2.0d);
|
||||||
RouteCandidate longerWithoutConflict =
|
RouteCandidate longerWithoutConflict =
|
||||||
new RouteCandidate(Arrays.asList("A", "C", "D"), 2.8d);
|
new RouteCandidate(Arrays.asList("A", "C", "D"), 2.0d * Math.sqrt(2.0d));
|
||||||
RouteCandidateSet candidates = new RouteCandidateSet(
|
RouteCandidateSet candidates = new RouteCandidateSet(
|
||||||
shortestWithOppositeConflict.getPath(), 2.0d,
|
shortestWithOppositeConflict.getPath(), 2.0d,
|
||||||
Arrays.asList(shortestWithOppositeConflict, longerWithoutConflict), false);
|
Arrays.asList(shortestWithOppositeConflict, longerWithoutConflict), false);
|
||||||
@@ -53,14 +54,15 @@ class RouteQualityEvaluatorTest {
|
|||||||
assertEquals(Arrays.asList("A", "C", "D"), decision.getSelectedPath());
|
assertEquals(Arrays.asList("A", "C", "D"), decision.getSelectedPath());
|
||||||
assertEquals("FEWER_OPPOSITE_DIRECTION_CONFLICTS", decision.getDecisionReason());
|
assertEquals("FEWER_OPPOSITE_DIRECTION_CONFLICTS", decision.getDecisionReason());
|
||||||
assertEquals(2, decision.getEvaluations().size());
|
assertEquals(2, decision.getEvaluations().size());
|
||||||
assertEquals(1.4d, decision.getDetourRatio(), 0.000001d);
|
assertEquals(Math.sqrt(2.0d), decision.getDetourRatio(), 0.000001d);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void lowerTupleFieldsDecideOnlyWhenAllHigherFieldsAreEqual() {
|
void lowerTupleFieldsDecideOnlyWhenAllHigherFieldsAreEqual() {
|
||||||
GridMap map = map();
|
GridMap map = map();
|
||||||
RouteCandidate occupied = new RouteCandidate(Arrays.asList("A", "B", "D"), 2.0d);
|
RouteCandidate occupied = new RouteCandidate(Arrays.asList("A", "B", "D"), 2.0d);
|
||||||
RouteCandidate reserved = new RouteCandidate(Arrays.asList("A", "C", "D"), 2.8d);
|
RouteCandidate reserved = new RouteCandidate(Arrays.asList("A", "C", "D"),
|
||||||
|
2.0d * Math.sqrt(2.0d));
|
||||||
TrafficRouteSnapshot snapshot = TrafficRouteSnapshot.builder()
|
TrafficRouteSnapshot snapshot = TrafficRouteSnapshot.builder()
|
||||||
.occupyNode("B", "OTHER-AGV")
|
.occupyNode("B", "OTHER-AGV")
|
||||||
.reserveEdge("A", "C", "OTHER-TASK")
|
.reserveEdge("A", "C", "OTHER-TASK")
|
||||||
@@ -95,16 +97,33 @@ class RouteQualityEvaluatorTest {
|
|||||||
map, turning, snapshot, "TASK-1", "AGV-1");
|
map, turning, snapshot, "TASK-1", "AGV-1");
|
||||||
|
|
||||||
assertEquals(1, quality.getTurnCount());
|
assertEquals(1, quality.getTurnCount());
|
||||||
|
assertEquals(1, quality.getOppositeDirectionConflictCount());
|
||||||
assertEquals(1, quality.getActiveRouteOverlapEdgeCount());
|
assertEquals(1, quality.getActiveRouteOverlapEdgeCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsUnknownNonAdjacentOrMislabeledDistanceCandidates() {
|
||||||
|
GridMap map = map();
|
||||||
|
RouteQualityEvaluator evaluator = new RouteQualityEvaluator();
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> evaluator.evaluate(
|
||||||
|
map, new RouteCandidate(Arrays.asList("A", "UNKNOWN", "D"), 2.0d),
|
||||||
|
TrafficRouteSnapshot.empty(), "TASK-1", "AGV-1"));
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> evaluator.evaluate(
|
||||||
|
map, new RouteCandidate(Arrays.asList("A", "D"), 2.0d),
|
||||||
|
TrafficRouteSnapshot.empty(), "TASK-1", "AGV-1"));
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> evaluator.evaluate(
|
||||||
|
map, new RouteCandidate(Arrays.asList("A", "B", "D"), 3.0d),
|
||||||
|
TrafficRouteSnapshot.empty(), "TASK-1", "AGV-1"));
|
||||||
|
}
|
||||||
|
|
||||||
private GridMap map() {
|
private GridMap map() {
|
||||||
GridMap map = new GridMap();
|
GridMap map = new GridMap();
|
||||||
add(map, "A", 0, 0);
|
add(map, "A", 0, 0);
|
||||||
add(map, "B", 1, 0);
|
add(map, "B", 1, 0);
|
||||||
add(map, "C", 1, 1);
|
add(map, "C", 1, 1);
|
||||||
add(map, "D", 2, 0);
|
add(map, "D", 2, 0);
|
||||||
add(map, "E", 1, -1);
|
add(map, "E", 1, 0);
|
||||||
add(map, "F", 1, 1);
|
add(map, "F", 1, 1);
|
||||||
map.connect("A", "B");
|
map.connect("A", "B");
|
||||||
map.connect("B", "D");
|
map.connect("B", "D");
|
||||||
|
|||||||
Reference in New Issue
Block a user