feat(javaTest): map simulation evidence for visualization

This commit is contained in:
zgw
2026-08-05 15:08:31 +08:00
parent ca532f3c05
commit a0453d8b22
2 changed files with 484 additions and 0 deletions

View File

@@ -0,0 +1,371 @@
package com.ft546.wcs.shadow.history.visualizer;
import com.ft546.wcs.demo.model.Site;
import com.ft546.wcs.demo.model.VehicleSnapshot;
import com.ft546.wcs.shadow.history.HistoricalQueueDataset;
import com.ft546.wcs.shadow.history.HistoricalQueueRecord;
import com.ft546.wcs.shadow.history.simulation.HistoricalDispatchSimulationResult;
import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationInput;
import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationTask;
import com.ft546.wcs.shadow.history.simulation.SimulationInvariantSummary;
import com.ft546.wcs.shadow.history.simulation.SimulationMetrics;
import com.ft546.wcs.shadow.history.simulation.SimulationTaskResult;
import com.ft546.wcs.shadow.history.simulation.SimulationTaskStatus;
import com.ft546.wcs.shadow.history.simulation.SimulationTickEvent;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
/**
* 将调度仿真证据转换为前端可序列化的数据结构。
*
* <p>该类不调用规划器,也不修改仿真结果。页面看到的车辆、路径和事件均来自同一次 Java 仿真。
*/
public final class SimulationVisualizationMapper {
private static final String MODE = "OFFLINE_IN_MEMORY_SIMULATION";
private static final String BOUNDARY =
"纯内存离线仿真,不连接生产 SQL Server、PLC 或 WMS,也不替代现场 HIL/ACTIVE 验收";
private static final String PLANNER_VERSION = "GridPathPlanner-BFS/full-grid-v1";
private static final DateTimeFormatter TIME =
DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss");
private final HistoricalQueueDataset dataset;
public SimulationVisualizationMapper(HistoricalQueueDataset dataset) {
if (dataset == null) throw new IllegalArgumentException("dataset is required");
this.dataset = dataset;
}
/** 返回页面启动所需的地图、站点和初始车辆元数据。 */
public Map<String, Object> bootstrap(HistoricalSimulationInput input) {
requireInput(input);
Map<String, Object> payload = new LinkedHashMap<String, Object>();
payload.put("mode", MODE);
payload.put("boundary", BOUNDARY);
payload.put("plannerVersion", PLANNER_VERSION);
payload.put("mapSha256", dataset.getFullMapSha256());
payload.put("fullMapRows", dataset.getFullMapRows());
payload.put("passableMapRows", input.getGridMap().getSites().size());
payload.put("database", dataset.getDatabase());
payload.put("sourceTable", dataset.getSourceTable());
payload.put("pathInfoUsedForPlanning", false);
List<Site> orderedSites = new ArrayList<Site>(input.getGridMap().getSites().values());
Collections.sort(orderedSites, Comparator.comparing(Site::getId));
List<Map<String, Object>> sites = new ArrayList<Map<String, Object>>();
Set<Integer> floors = new TreeSet<Integer>();
for (Site site : orderedSites) {
sites.add(site(site));
floors.add(site.getFloor());
}
payload.put("floors", new ArrayList<Integer>(floors));
payload.put("sites", sites);
List<VehicleSnapshot> orderedVehicles =
new ArrayList<VehicleSnapshot>(input.getInitialVehicles());
Collections.sort(orderedVehicles, Comparator.comparing(VehicleSnapshot::getVehicleId));
List<Map<String, Object>> vehicles = new ArrayList<Map<String, Object>>();
for (VehicleSnapshot vehicle : orderedVehicles) vehicles.add(vehicle(vehicle));
payload.put("vehicles", vehicles);
return payload;
}
/** 返回一次仿真的任务、事件、指标、归档和安全证据。 */
public Map<String, Object> result(HistoricalSimulationInput input,
HistoricalDispatchSimulationResult result) {
requireInput(input);
if (result == null) throw new IllegalArgumentException("result is required");
Map<String, HistoricalSimulationTask> definitions = taskDefinitions(input);
List<SimulationTaskResult> orderedTasks =
new ArrayList<SimulationTaskResult>(result.getTaskResults());
Collections.sort(orderedTasks,
Comparator.comparing(SimulationTaskResult::getSimulationTaskId));
List<Map<String, Object>> tasks = new ArrayList<Map<String, Object>>();
for (SimulationTaskResult task : orderedTasks) {
tasks.add(task(task, definitions.get(task.getSimulationTaskId()),
result.getAcceleration()));
}
List<Map<String, Object>> events = new ArrayList<Map<String, Object>>();
for (SimulationTickEvent event : result.getEvents()) events.add(event(event));
Map<String, Object> payload = new LinkedHashMap<String, Object>();
payload.put("mode", MODE);
payload.put("boundary", BOUNDARY);
payload.put("plannerVersion", PLANNER_VERSION);
payload.put("mapSha256", dataset.getFullMapSha256());
payload.put("pathInfoUsedForPlanning", false);
payload.put("summary", summary(result));
payload.put("tasks", tasks);
payload.put("events", events);
payload.put("invariants", invariants(result.getInvariants()));
payload.put("archive", archive(result));
payload.put("wms", wms(result));
return payload;
}
private Map<String, Object> summary(HistoricalDispatchSimulationResult result) {
Map<String, Object> value = new LinkedHashMap<String, Object>();
value.put("inputCount", result.getTaskResults().size());
value.put("acceleration", result.getAcceleration());
value.put("completedCount", result.getCompletedCount());
value.put("waitingCount", result.getWaitingCount());
value.put("failedCount", result.getFailedCount());
value.put("completedByVehicle", result.completedByVehicle());
value.put("reassignmentCount", result.getReassignmentCount());
value.put("replannedPathDifferenceCount", result.getReplannedCount());
value.put("retryCount", result.getRetryCount());
value.put("tickCount", result.getTickCount());
value.put("waitingTime", timeStats(waitingSamples(result)));
value.put("completionTime", timeStats(completionSamples(result)));
value.put("historicalDuration", timeStats(historicalSamples(result)));
value.put("simulationMinusHistoricalDuration",
timeStats(durationDifferenceSamples(result)));
value.put("holdReasons", aggregateReasons(result, true));
value.put("retryReasons", aggregateReasons(result, false));
value.put("failureReasons", failureReasons(result));
boolean safe = result.getInvariants().totalSafetyViolations() == 0
&& result.getInvariants().getDeadlocks() == 0;
value.put("acceptancePassed", safe && result.getWaitingCount() == 0
&& result.getFailedCount() == 0);
return value;
}
private Map<String, Object> task(SimulationTaskResult task,
HistoricalSimulationTask definition,
int acceleration) {
HistoricalQueueRecord record = task.getRecord();
List<String> historicalPath = historicalPath(record.getPathInfo());
Long historicalDuration = historicalDuration(task);
Long simulatedDuration = task.getCompletionMillis();
Long scaledDuration = simulatedDuration == null
? null : simulatedDuration * acceleration;
Map<String, Object> value = new LinkedHashMap<String, Object>();
value.put("simulationTaskId", task.getSimulationTaskId());
value.put("taskNo", record.getTaskNo());
value.put("guid", record.getGuid());
value.put("taskType", definition == null
? record.getTaskType() : definition.getTaskType().name());
value.put("priority", record.getPriority());
value.put("businessStartSite", record.getStartSite());
value.put("businessEndSite", record.getEndSite());
value.put("physicalStartSite", definition == null
? record.getStartSite() : definition.getPhysicalSource());
value.put("physicalEndSite", definition == null
? record.getEndSite() : definition.getPhysicalTarget());
value.put("historicalVehicleId", record.getAgvId());
value.put("simulatedVehicleId", task.getSimulatedVehicleId());
value.put("historicalPath", historicalPath);
value.put("plannedPath", task.getPlannedPath());
value.put("assignedVehicles", task.getAssignedVehicles());
value.put("status", task.getStatus().name());
value.put("reason", task.getReason());
value.put("arrivalMillis", task.getArrivalMillis());
value.put("firstAssignedMillis", task.getFirstAssignedMillis());
value.put("completedMillis", task.getCompletedMillis());
value.put("waitingMillis", task.getWaitingMillis());
value.put("simulationDurationMillis", simulatedDuration);
value.put("historicalDurationMillis", historicalDuration);
value.put("scaledSourceDurationMillis", scaledDuration);
value.put("durationDifferenceMillis", historicalDuration == null || scaledDuration == null
? null : scaledDuration - historicalDuration);
value.put("holdCount", task.getHoldCount());
value.put("protocolAttempts", task.getProtocolAttempts());
value.put("retryCount", task.getRetryCount());
value.put("holdReasons", task.getHoldReasons());
value.put("retryReasons", task.getRetryReasons());
value.put("vehicleChanged", task.getSimulatedVehicleId() != null
&& !task.getSimulatedVehicleId().equals(record.getAgvId()));
value.put("pathChanged", !historicalPath.equals(task.getPlannedPath()));
return value;
}
private Map<String, Object> event(SimulationTickEvent event) {
Map<String, Object> value = new LinkedHashMap<String, Object>();
value.put("tick", event.getTick());
value.put("simulationMillis", event.getSimulationMillis());
value.put("eventType", event.getType());
value.put("taskId", event.getTaskId());
value.put("vehicleId", event.getVehicleId());
value.put("reason", event.getReason());
value.put("fromSite", event.getFromSite());
value.put("toSite", event.getToSite());
return value;
}
private Map<String, Object> site(Site site) {
Map<String, Object> value = new LinkedHashMap<String, Object>();
value.put("siteId", site.getId());
value.put("floor", site.getFloor());
value.put("x", site.getX());
value.put("y", site.getY());
value.put("channel", site.getChannel());
return value;
}
private Map<String, Object> vehicle(VehicleSnapshot vehicle) {
Map<String, Object> value = new LinkedHashMap<String, Object>();
value.put("vehicleId", vehicle.getVehicleId());
value.put("floor", vehicle.getFloor());
value.put("currentSite", vehicle.getCurrentSite());
value.put("workMode", vehicle.getWorkMode());
value.put("status", vehicle.getStatus());
value.put("battery", vehicle.getBattery());
value.put("connected", vehicle.isConnected());
value.put("allowSend", vehicle.isAllowSend());
value.put("cargo", vehicle.isCargo());
return value;
}
private Map<String, Object> invariants(SimulationInvariantSummary source) {
Map<String, Object> value = new LinkedHashMap<String, Object>();
value.put("capacityViolations", source.getCapacityViolations());
value.put("nodeDoubleOccupancyViolations", source.getNodeDoubleOccupancyViolations());
value.put("oppositeEdgeViolations", source.getOppositeEdgeViolations());
value.put("duplicateArchiveAttempts", source.getDuplicateArchiveAttempts());
value.put("duplicateWmsBusinessEffects", source.getDuplicateWmsBusinessEffects());
value.put("deadlocks", source.getDeadlocks());
value.put("totalSafetyViolations", source.totalSafetyViolations());
value.put("passed", source.totalSafetyViolations() == 0 && source.getDeadlocks() == 0);
return value;
}
private Map<String, Object> archive(HistoricalDispatchSimulationResult result) {
Map<String, Object> value = new LinkedHashMap<String, Object>();
value.put("count", result.getArchiveCount());
value.put("expected", result.getCompletedCount());
value.put("complete", result.getArchiveCount() == result.getCompletedCount()
&& result.getInvariants().getDuplicateArchiveAttempts() == 0);
value.put("duplicateAttempts", result.getInvariants().getDuplicateArchiveAttempts());
return value;
}
private Map<String, Object> wms(HistoricalDispatchSimulationResult result) {
Map<String, Object> value = new LinkedHashMap<String, Object>();
value.put("deliveryAttempts", result.getWmsDeliveryAttempts());
value.put("businessEffects", result.getWmsBusinessEffects());
value.put("duplicateDeliveryAttempts", Math.max(0,
result.getWmsDeliveryAttempts() - result.getCompletedCount()));
value.put("duplicateBusinessEffects",
result.getInvariants().getDuplicateWmsBusinessEffects());
value.put("idempotent", result.getWmsBusinessEffects() == result.getCompletedCount()
&& result.getInvariants().getDuplicateWmsBusinessEffects() == 0);
return value;
}
private Map<String, Object> timeStats(List<Long> samples) {
SimulationMetrics metrics = new SimulationMetrics();
Map<String, Object> value = new LinkedHashMap<String, Object>();
value.put("sampleCount", samples.size());
value.put("averageMillis", metrics.average(samples));
value.put("p95Millis", metrics.percentile(samples, 0.95));
value.put("p99Millis", metrics.percentile(samples, 0.99));
return value;
}
private List<Long> waitingSamples(HistoricalDispatchSimulationResult result) {
List<Long> values = new ArrayList<Long>();
for (SimulationTaskResult task : result.getTaskResults()) {
if (task.getWaitingMillis() != null) values.add(task.getWaitingMillis());
}
return values;
}
private List<Long> completionSamples(HistoricalDispatchSimulationResult result) {
List<Long> values = new ArrayList<Long>();
for (SimulationTaskResult task : result.getTaskResults()) {
if (task.getCompletionMillis() != null) values.add(task.getCompletionMillis());
}
return values;
}
private List<Long> historicalSamples(HistoricalDispatchSimulationResult result) {
List<Long> values = new ArrayList<Long>();
for (SimulationTaskResult task : result.getTaskResults()) {
Long duration = historicalDuration(task);
if (duration != null) values.add(duration);
}
return values;
}
private List<Long> durationDifferenceSamples(HistoricalDispatchSimulationResult result) {
List<Long> values = new ArrayList<Long>();
for (SimulationTaskResult task : result.getTaskResults()) {
Long historical = historicalDuration(task);
Long simulated = task.getCompletionMillis();
if (historical != null && simulated != null) {
values.add(simulated * result.getAcceleration() - historical);
}
}
return values;
}
private Map<String, Integer> aggregateReasons(HistoricalDispatchSimulationResult result,
boolean holds) {
Map<String, Integer> values = new LinkedHashMap<String, Integer>();
for (SimulationTaskResult task : result.getTaskResults()) {
Map<String, Integer> source = holds ? task.getHoldReasons() : task.getRetryReasons();
for (Map.Entry<String, Integer> entry : source.entrySet()) {
Integer previous = values.get(entry.getKey());
values.put(entry.getKey(), (previous == null ? 0 : previous) + entry.getValue());
}
}
return values;
}
private Map<String, Integer> failureReasons(HistoricalDispatchSimulationResult result) {
Map<String, Integer> values = new LinkedHashMap<String, Integer>();
for (SimulationTaskResult task : result.getTaskResults()) {
if (task.getStatus() != SimulationTaskStatus.FAILED) continue;
Integer previous = values.get(task.getReason());
values.put(task.getReason(), previous == null ? 1 : previous + 1);
}
return values;
}
private Long historicalDuration(SimulationTaskResult task) {
try {
LocalDateTime created = LocalDateTime.parse(task.getRecord().getCreateDate(), TIME);
LocalDateTime finished = LocalDateTime.parse(task.getRecord().getFinishDate(), TIME);
return Duration.between(created, finished).toMillis();
} catch (DateTimeParseException | NullPointerException exception) {
return null;
}
}
private List<String> historicalPath(String pathInfo) {
if (pathInfo == null || pathInfo.trim().isEmpty()) return Collections.emptyList();
List<String> values = new ArrayList<String>();
for (String node : Arrays.asList(pathInfo.split(","))) {
String normalized = node.trim();
if (!normalized.isEmpty()) values.add(normalized);
}
return Collections.unmodifiableList(values);
}
private Map<String, HistoricalSimulationTask> taskDefinitions(HistoricalSimulationInput input) {
Map<String, HistoricalSimulationTask> values =
new LinkedHashMap<String, HistoricalSimulationTask>();
for (HistoricalSimulationTask task : input.getTasks()) {
values.put(task.getSimulationTaskId(), task);
}
return values;
}
private void requireInput(HistoricalSimulationInput input) {
if (input == null) throw new IllegalArgumentException("input is required");
}
}

View File

@@ -0,0 +1,113 @@
package com.ft546.wcs.shadow.history.visualizer;
import com.ft546.wcs.shadow.history.HistoricalQueueDataset;
import com.ft546.wcs.shadow.history.HistoricalQueueLoader;
import com.ft546.wcs.shadow.history.simulation.HistoricalDispatchSimulationResult;
import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationInput;
import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationInputFactory;
import com.ft546.wcs.shadow.history.simulation.HistoricalSimulationTask;
import com.ft546.wcs.shadow.history.simulation.SimulationTaskResult;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class SimulationVisualizationMapperTest {
private static HistoricalQueueDataset dataset;
private static HistoricalSimulationInput input;
private static HistoricalDispatchSimulationResult result;
private static HistoricalSimulationTask sampleTask;
@BeforeAll
static void simulateOneTask() {
dataset = new HistoricalQueueLoader().load(
SimulationVisualizationMapperTest.class.getClassLoader(), "shadow/history");
input = new HistoricalSimulationInputFactory().create(dataset);
sampleTask = input.getTasks().stream()
.filter(task -> !task.getPhysicalSource().equals(task.getPhysicalTarget()))
.findFirst()
.orElseThrow(() -> new AssertionError("a point-to-point sample is required"));
PointToPointSimulationRequest request = PointToPointSimulationRequest.valid(
sampleTask.getPhysicalSource(), sampleTask.getPhysicalTarget(),
"RELOCATION", 2, 100, "HISTORY-CAR",
Arrays.asList("WRONG", "REFERENCE"), "PLC_FIRST_SEND_FAILURE");
result = new PointToPointSimulationService(input).run(request);
}
@Test
void bootstrapReturnsSortedSearchableSitesAndInitialVehicles() {
Map<String, Object> payload = mapper().bootstrap(input);
List<Map<String, Object>> sites = listOfMaps(payload.get("sites"));
List<Map<String, Object>> vehicles = listOfMaps(payload.get("vehicles"));
assertEquals("OFFLINE_IN_MEMORY_SIMULATION", payload.get("mode"));
assertEquals(dataset.getFullMapSha256(), payload.get("mapSha256"));
assertEquals(input.getGridMap().getSites().size(), sites.size());
assertEquals(input.getInitialVehicles().size(), vehicles.size());
assertTrue(String.valueOf(sites.get(0).get("siteId"))
.compareTo(String.valueOf(sites.get(sites.size() - 1).get("siteId"))) < 0);
assertTrue(sites.get(0).containsKey("x"));
assertTrue(sites.get(0).containsKey("y"));
assertTrue(sites.get(0).containsKey("floor"));
}
@Test
void mapsPlannerOutputEventsAndSafetyEvidenceWithoutReplanning() {
Map<String, Object> payload = mapper().result(input, result);
List<Map<String, Object>> tasks = listOfMaps(payload.get("tasks"));
List<Map<String, Object>> events = listOfMaps(payload.get("events"));
Map<String, Object> summary = map(payload.get("summary"));
Map<String, Object> invariants = map(payload.get("invariants"));
SimulationTaskResult sourceTask = result.getTaskResults().get(0);
assertEquals(1, tasks.size());
assertEquals(sourceTask.getPlannedPath(), tasks.get(0).get("plannedPath"));
assertEquals(Arrays.asList("WRONG", "REFERENCE"), tasks.get(0).get("historicalPath"));
assertEquals(sampleTask.getPhysicalSource(), tasks.get(0).get("physicalStartSite"));
assertEquals(sampleTask.getPhysicalTarget(), tasks.get(0).get("physicalEndSite"));
assertEquals(Boolean.TRUE, tasks.get(0).get("vehicleChanged"));
assertEquals(Boolean.TRUE, tasks.get(0).get("pathChanged"));
assertTrue(events.stream().anyMatch(event -> "ASSIGN".equals(event.get("eventType"))));
assertTrue(events.stream().anyMatch(event -> "MOVE".equals(event.get("eventType"))));
assertTrue(events.stream().anyMatch(event -> "PLC_RETRY".equals(event.get("eventType"))));
assertEquals(1, summary.get("completedCount"));
assertEquals(0, summary.get("waitingCount"));
assertEquals(0, summary.get("failedCount"));
assertEquals(0, invariants.get("totalSafetyViolations"));
assertEquals(Boolean.TRUE, map(payload.get("archive")).get("complete"));
assertEquals(Boolean.TRUE, map(payload.get("wms")).get("idempotent"));
}
@Test
void timingStatisticsUseOnlyCompletedSamples() {
Map<String, Object> summary = map(mapper().result(input, result).get("summary"));
Map<String, Object> waiting = map(summary.get("waitingTime"));
Map<String, Object> completion = map(summary.get("completionTime"));
assertEquals(1, waiting.get("sampleCount"));
assertEquals(1, completion.get("sampleCount"));
assertFalse(Collections.singletonList(waiting.get("averageMillis")).contains(null));
assertFalse(Collections.singletonList(completion.get("p95Millis")).contains(null));
}
private SimulationVisualizationMapper mapper() {
return new SimulationVisualizationMapper(dataset);
}
@SuppressWarnings("unchecked")
private static List<Map<String, Object>> listOfMaps(Object value) {
return (List<Map<String, Object>>) value;
}
@SuppressWarnings("unchecked")
private static Map<String, Object> map(Object value) {
return (Map<String, Object>) value;
}
}