feat(javaTest): plan history tasks from physical endpoints

This commit is contained in:
zgw
2026-07-31 14:07:56 +08:00
parent 5ae50a85e2
commit a4e63ebb9a
4 changed files with 180 additions and 104 deletions

View File

@@ -1,27 +1,16 @@
package com.ft546.wcs.shadow.history.simulation; package com.ft546.wcs.shadow.history.simulation;
import com.ft546.wcs.demo.model.DispatchTask; 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.model.VehicleSnapshot;
import com.ft546.wcs.demo.traffic.GridMap; import com.ft546.wcs.shadow.history.HistoricalMapCell;
import com.ft546.wcs.shadow.history.HistoricalMapSite;
import com.ft546.wcs.shadow.history.HistoricalPath; import com.ft546.wcs.shadow.history.HistoricalPath;
import com.ft546.wcs.shadow.history.HistoricalQueueDataset; import com.ft546.wcs.shadow.history.HistoricalQueueDataset;
import com.ft546.wcs.shadow.history.HistoricalQueueNormalizer; import com.ft546.wcs.shadow.history.HistoricalQueueNormalizer;
import com.ft546.wcs.shadow.history.HistoricalQueueRecord; import com.ft546.wcs.shadow.history.HistoricalQueueRecord;
import com.ft546.wcs.shadow.map.EffectiveTrafficMap;
import com.ft546.wcs.shadow.map.TrafficMapCompiler;
import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set;
public final class HistoricalSimulationInputFactory { public final class HistoricalSimulationInputFactory {
public HistoricalSimulationInput create(HistoricalQueueDataset dataset) { public HistoricalSimulationInput create(HistoricalQueueDataset dataset) {
@@ -39,107 +28,52 @@ public final class HistoricalSimulationInputFactory {
List<HistoricalSimulationTask> tasks = new ArrayList<HistoricalSimulationTask>(); List<HistoricalSimulationTask> tasks = new ArrayList<HistoricalSimulationTask>();
List<HistoricalSimulationFailure> failures = new ArrayList<HistoricalSimulationFailure>(); List<HistoricalSimulationFailure> failures = new ArrayList<HistoricalSimulationFailure>();
HistoricalGridMapCompiler.CompiledHistoricalMap compiled =
new HistoricalGridMapCompiler().compile(dataset.getFullMapCells());
PhysicalEndpointResolver endpointResolver = new PhysicalEndpointResolver();
int sequence = 0; int sequence = 0;
for (HistoricalPath path : paths) { for (HistoricalPath path : paths) {
HistoricalQueueRecord record = path.getRecord(); HistoricalQueueRecord record = path.getRecord();
int stableSequence = sequence++; int stableSequence = sequence++;
String id = simulationId(record, stableSequence); String id = simulationId(record, stableSequence);
DispatchTask.TaskType type = taskType(record.getTaskType()); DispatchTask.TaskType type = taskType(record.getTaskType());
if (!path.isReplayable() || type == null) { if (type == null) {
failures.add(new HistoricalSimulationFailure(id, record, failureReason(path, type))); failures.add(new HistoricalSimulationFailure(id, record, "INVALID_TASK_TYPE"));
continue; continue;
} }
List<String> nodes = path.getNodes(); PhysicalEndpointResolver.ResolvedEndpoints endpoints = endpointResolver.resolve(
String source = dataset.getMapSites().contains(record.getStartSite()) record, compiled.getPassableSites());
? record.getStartSite() : nodes.get(0); if (compiled.getComponentId(endpoints.getSource())
String target = dataset.getMapSites().contains(record.getEndSite()) != compiled.getComponentId(endpoints.getTarget())) {
? record.getEndSite() : nodes.get(nodes.size() - 1); throw new IllegalArgumentException("unreachable physical endpoints for " + id);
}
tasks.add(new HistoricalSimulationTask(id, record, type, tasks.add(new HistoricalSimulationTask(id, record, type,
record.getPriority() == null ? Integer.MAX_VALUE : record.getPriority(), record.getPriority() == null ? Integer.MAX_VALUE : record.getPriority(),
path.getCreatedAt(), path.getFinishedAt(), source, target, nodes, stableSequence)); path.getCreatedAt(), path.getFinishedAt(), endpoints.getSource(),
endpoints.getTarget(), path.getNodes(), stableSequence));
} }
GridMap gridMap = buildGridMap(tasks, dataset.getMapSiteDetails()); List<VehicleSnapshot> vehicles = initialVehicles(compiled);
EffectiveTrafficMap trafficMap = buildTrafficMap(tasks); return new HistoricalSimulationInput(tasks, failures, compiled.getGridMap(),
List<VehicleSnapshot> vehicles = initialVehicles(tasks, dataset.getMapSiteDetails()); compiled.getTrafficMap(), vehicles);
return new HistoricalSimulationInput(tasks, failures, gridMap, trafficMap, vehicles);
} }
private GridMap buildGridMap(List<HistoricalSimulationTask> tasks, private List<VehicleSnapshot> initialVehicles(
Map<String, HistoricalMapSite> details) { HistoricalGridMapCompiler.CompiledHistoricalMap compiled) {
LinkedHashSet<String> nodes = new LinkedHashSet<String>();
LinkedHashSet<String> edges = new LinkedHashSet<String>();
for (HistoricalSimulationTask task : tasks) {
List<String> path = task.getHistoricalNodes();
nodes.addAll(path);
for (int i = 1; i < path.size(); i++) {
edges.add(edgeId(path.get(i - 1), path.get(i)));
}
}
GridMap map = new GridMap();
for (String node : nodes) {
HistoricalMapSite detail = details.get(node);
map.addSite(new Site(node, detail.getZ(), detail.getX(), detail.getY(),
detail.getAxy().isEmpty() ? "OFFLINE" : detail.getAxy()));
}
for (String edge : edges) {
String[] pair = edge.split("\\|", -1);
map.connect(pair[0], pair[1]);
}
return map;
}
private EffectiveTrafficMap buildTrafficMap(List<HistoricalSimulationTask> tasks) {
LinkedHashSet<String> nodes = new LinkedHashSet<String>();
LinkedHashMap<String, TrafficMapCompiler.EdgeDefinition> edges =
new LinkedHashMap<String, TrafficMapCompiler.EdgeDefinition>();
for (HistoricalSimulationTask task : tasks) {
List<String> path = task.getHistoricalNodes();
nodes.addAll(path);
for (int i = 1; i < path.size(); i++) {
String id = edgeId(path.get(i - 1), path.get(i));
String[] pair = id.split("\\|", -1);
edges.put(id, new TrafficMapCompiler.EdgeDefinition(pair[0], pair[1]));
}
}
TrafficMapCompiler.MapDefinition definition = new TrafficMapCompiler.MapDefinition(
"FT546-HISTORY-SIM", 1L, "OFFLINE", true,
"history-snapshot-v1", "history-snapshot-v1", nodes,
new LinkedHashSet<TrafficMapCompiler.EdgeDefinition>(edges.values()),
Collections.emptyMap(), Collections.emptySet(), Collections.emptySet());
return new TrafficMapCompiler().compile(definition);
}
private List<VehicleSnapshot> initialVehicles(List<HistoricalSimulationTask> tasks,
Map<String, HistoricalMapSite> details) {
Map<String, HistoricalSimulationTask> earliest =
new HashMap<String, HistoricalSimulationTask>();
for (HistoricalSimulationTask task : tasks) {
String vehicle = task.getRecord().getAgvId();
if (vehicle != null && (!earliest.containsKey(vehicle)
|| task.getCreatedAt().isBefore(earliest.get(vehicle).getCreatedAt()))) {
earliest.put(vehicle, task);
}
}
List<String> ids = new ArrayList<String>(earliest.keySet());
Collections.sort(ids);
List<VehicleSnapshot> vehicles = new ArrayList<VehicleSnapshot>(); List<VehicleSnapshot> vehicles = new ArrayList<VehicleSnapshot>();
for (String id : ids) { for (int floor = 1; floor <= 3; floor++) {
String site = earliest.get(id).getPhysicalSource(); String id = String.valueOf(floor);
vehicles.add(new VehicleSnapshot(id, details.get(site).getZ(), site, String site = "01070" + floor;
HistoricalMapCell cell = compiled.getPassableSites().get(site);
if (cell == null || cell.getZ() != floor) {
throw new IllegalArgumentException("missing fleet start site " + site);
}
vehicles.add(new VehicleSnapshot(id, floor, site,
5, 1, 100, true, true, false)); 5, 1, 100, true, true, false));
} }
return vehicles; return vehicles;
} }
private String failureReason(HistoricalPath path, DispatchTask.TaskType type) {
if (type == null) return "INVALID_TASK_TYPE";
for (String issue : path.getIssues()) {
if (issue.startsWith("UNKNOWN_MAP_SITE")) return "UNKNOWN_MAP";
}
return "INPUT_REJECTED";
}
private DispatchTask.TaskType taskType(String value) { private DispatchTask.TaskType taskType(String value) {
if ("1".equals(value)) return DispatchTask.TaskType.INBOUND; if ("1".equals(value)) return DispatchTask.TaskType.INBOUND;
if ("2".equals(value)) return DispatchTask.TaskType.OUTBOUND; if ("2".equals(value)) return DispatchTask.TaskType.OUTBOUND;
@@ -151,9 +85,5 @@ public final class HistoricalSimulationInputFactory {
return text(record.getTaskNo()) + "@" + text(record.getGuid()) + "#" + stableSequence; return text(record.getTaskNo()) + "@" + text(record.getGuid()) + "#" + stableSequence;
} }
private static String edgeId(String left, String right) {
return left.compareTo(right) <= 0 ? left + "|" + right : right + "|" + left;
}
private static String text(String value) { return value == null ? "" : value; } private static String text(String value) { return value == null ? "" : value; }
} }

View File

@@ -0,0 +1,56 @@
package com.ft546.wcs.shadow.history.simulation;
import com.ft546.wcs.shadow.history.HistoricalMapCell;
import com.ft546.wcs.shadow.history.HistoricalQueueRecord;
import java.util.Map;
public final class PhysicalEndpointResolver {
public ResolvedEndpoints resolve(HistoricalQueueRecord record,
Map<String, HistoricalMapCell> passableSites) {
if (record == null || passableSites == null) {
throw new IllegalArgumentException("record and passable sites are required");
}
HistoricalMapCell source = passableSites.get(record.getStartSite());
if (source == null) {
throw new IllegalArgumentException("unknown physical source " + record.getStartSite());
}
int floor = source.getZ();
boolean declaredFloorConsistent = true;
if (record.getFloor() != null && !record.getFloor().trim().isEmpty()) {
try {
declaredFloorConsistent = Integer.parseInt(record.getFloor().trim()) == floor;
} catch (NumberFormatException ex) {
declaredFloorConsistent = false;
}
}
String targetId = "tsj".equals(record.getEndSite())
? "07050" + floor : record.getEndSite();
HistoricalMapCell target = passableSites.get(targetId);
if (target == null) {
throw new IllegalArgumentException("unknown physical target " + targetId);
}
return new ResolvedEndpoints(source.getSiteId(), target.getSiteId(), floor,
declaredFloorConsistent);
}
public static final class ResolvedEndpoints {
private final String source;
private final String target;
private final int floor;
private final boolean declaredFloorConsistent;
private ResolvedEndpoints(String source, String target, int floor,
boolean declaredFloorConsistent) {
this.source = source;
this.target = target;
this.floor = floor;
this.declaredFloorConsistent = declaredFloorConsistent;
}
public String getSource() { return source; }
public String getTarget() { return target; }
public int getFloor() { return floor; }
public boolean isDeclaredFloorConsistent() { return declaredFloorConsistent; }
}
}

View File

@@ -3,11 +3,14 @@ package com.ft546.wcs.shadow.history.simulation;
import com.ft546.wcs.demo.model.DispatchTask; import com.ft546.wcs.demo.model.DispatchTask;
import com.ft546.wcs.shadow.history.HistoricalQueueDataset; import com.ft546.wcs.shadow.history.HistoricalQueueDataset;
import com.ft546.wcs.shadow.history.HistoricalQueueLoader; import com.ft546.wcs.shadow.history.HistoricalQueueLoader;
import com.ft546.wcs.shadow.history.HistoricalQueueRecord;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.Arrays; import java.util.Arrays;
import java.util.ArrayList;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -26,12 +29,10 @@ class HistoricalSimulationInputFactoryTest {
@Test @Test
void convertsEveryHistoryRowToOneUniqueSchedulableOrFailedInput() { void convertsEveryHistoryRowToOneUniqueSchedulableOrFailedInput() {
assertEquals(238, input.getTasks().size()); assertEquals(240, input.getTasks().size());
assertEquals(2, input.getFailedInputs().size()); assertEquals(0, input.getFailedInputs().size());
assertEquals(240, input.allSimulationIds().size()); assertEquals(240, input.allSimulationIds().size());
assertEquals(240, input.allSimulationIds().stream().distinct().count()); assertEquals(240, input.allSimulationIds().stream().distinct().count());
assertTrue(input.getFailedInputs().stream()
.allMatch(failure -> "UNKNOWN_MAP".equals(failure.getReason())));
} }
@Test @Test
@@ -41,7 +42,7 @@ class HistoricalSimulationInputFactoryTest {
.findFirst().get(); .findFirst().get();
assertEquals("tsj", task.getRecord().getEndSite()); assertEquals("tsj", task.getRecord().getEndSite());
assertFalse("tsj".equals(task.getPhysicalTarget())); assertFalse("tsj".equals(task.getPhysicalTarget()));
assertEquals(task.getHistoricalNodes().get(task.getHistoricalNodes().size() - 1), assertEquals("07050" + input.getGridMap().getSite(task.getPhysicalSource()).getFloor(),
task.getPhysicalTarget()); task.getPhysicalTarget());
assertFalse(task.getHistoricalNodes().isEmpty()); assertFalse(task.getHistoricalNodes().isEmpty());
} }
@@ -50,9 +51,43 @@ class HistoricalSimulationInputFactoryTest {
void buildsReplannableTopologyAndInitializesTheHistoricalFleet() { void buildsReplannableTopologyAndInitializesTheHistoricalFleet() {
assertEquals(EnumSet.allOf(DispatchTask.TaskType.class), input.getTasks().stream() assertEquals(EnumSet.allOf(DispatchTask.TaskType.class), input.getTasks().stream()
.map(HistoricalSimulationTask::getTaskType).collect(Collectors.toSet())); .map(HistoricalSimulationTask::getTaskType).collect(Collectors.toSet()));
assertTrue(input.getGridMap().getSites().size() > 100); assertEquals(1175, input.getGridMap().getSites().size());
assertTrue(input.getTrafficMap().getEdges().size() > 100); assertTrue(input.getTrafficMap().getEdges().size() > 1000);
assertEquals(Arrays.asList("1", "2", "3"), input.getInitialVehicles().stream() assertEquals(Arrays.asList("1", "2", "3"), input.getInitialVehicles().stream()
.map(vehicle -> vehicle.getVehicleId()).collect(Collectors.toList())); .map(vehicle -> vehicle.getVehicleId()).collect(Collectors.toList()));
} }
@Test
void changingHistoricalPathDoesNotChangePlanningInput() {
HistoricalQueueDataset original = new HistoricalQueueLoader().load(
getClass().getClassLoader(), "shadow/history");
List<HistoricalQueueRecord> changedRecords =
new ArrayList<HistoricalQueueRecord>(original.getRecords());
HistoricalQueueRecord first = changedRecords.get(0);
changedRecords.set(0, new HistoricalQueueRecord(first.getGuid(), first.getTaskNo(),
first.getTaskType(), first.getPriority(), first.getStartSite(), first.getEndSite(),
"FAKE_A,FAKE_B", first.getCreateDate(), first.getFinishDate(), first.getAgvId()));
HistoricalQueueDataset changed = copyWithRecords(original, changedRecords);
HistoricalSimulationInput originalInput = new HistoricalSimulationInputFactory().create(original);
HistoricalSimulationInput changedInput = new HistoricalSimulationInputFactory().create(changed);
assertEquals(originalInput.getTasks().size(), changedInput.getTasks().size());
assertEquals(originalInput.getFailedInputs().size(), changedInput.getFailedInputs().size());
assertEquals(originalInput.getGridMap().getSites().keySet(),
changedInput.getGridMap().getSites().keySet());
assertEquals(originalInput.getTrafficMap().getEdges(), changedInput.getTrafficMap().getEdges());
}
private HistoricalQueueDataset copyWithRecords(HistoricalQueueDataset original,
List<HistoricalQueueRecord> records) {
return new HistoricalQueueDataset(records, original.getMapSiteDetails(),
original.getFullMapCells(), original.getFullMapRows(), original.getFullMapSha256(),
original.getManifestSampleRows(), original.getFullScanTotalRows(),
original.getDatabase(), original.getSourceTable(), original.getExtractedAt(),
original.getMinCreateDate(), original.getMaxCreateDate(),
original.getMinFinishDate(), original.getMaxFinishDate(),
original.getMissingPathRows(), original.getMissingIntervalRows(),
original.getReversedIntervalRows());
}
} }

View File

@@ -0,0 +1,55 @@
package com.ft546.wcs.shadow.history.simulation;
import com.ft546.wcs.shadow.history.HistoricalMapCell;
import com.ft546.wcs.shadow.history.HistoricalQueueDataset;
import com.ft546.wcs.shadow.history.HistoricalQueueLoader;
import com.ft546.wcs.shadow.history.HistoricalQueueRecord;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
class PhysicalEndpointResolverTest {
@Test
void usesPhysicalSourceFloorWhenTheHistoricalFloorIsStale() {
HistoricalQueueDataset dataset = new HistoricalQueueLoader().load(
getClass().getClassLoader(), "shadow/history");
Map<String, HistoricalMapCell> sites = new HistoricalGridMapCompiler()
.compile(dataset.getFullMapCells()).getPassableSites();
HistoricalQueueRecord record = dataset.getRecords().stream()
.filter(item -> "090501".equals(item.getStartSite()))
.filter(item -> "070101".equals(item.getEndSite()))
.findFirst().get();
PhysicalEndpointResolver.ResolvedEndpoints endpoints =
new PhysicalEndpointResolver().resolve(record, sites);
assertEquals(1, endpoints.getFloor());
assertEquals("070101", endpoints.getTarget());
assertFalse(endpoints.isDeclaredFloorConsistent());
}
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
void resolvesTsjByTheProductionFloorRule(int floor) {
HistoricalQueueDataset dataset = new HistoricalQueueLoader().load(
getClass().getClassLoader(), "shadow/history");
Map<String, HistoricalMapCell> sites = new HistoricalGridMapCompiler()
.compile(dataset.getFullMapCells()).getPassableSites();
HistoricalQueueRecord record = dataset.getRecords().stream()
.filter(item -> "tsj".equals(item.getEndSite()))
.filter(item -> sites.get(item.getStartSite()).getZ() == floor)
.findFirst().get();
PhysicalEndpointResolver.ResolvedEndpoints endpoints =
new PhysicalEndpointResolver().resolve(record, sites);
assertEquals(record.getStartSite(), endpoints.getSource());
assertEquals("07050" + floor, endpoints.getTarget());
assertEquals(floor, endpoints.getFloor());
}
}