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");
}
}