diff --git a/javaTest/src/main/java/com/ft546/wcs/shadow/history/visualizer/SimulationVisualizationMapper.java b/javaTest/src/main/java/com/ft546/wcs/shadow/history/visualizer/SimulationVisualizationMapper.java new file mode 100644 index 0000000..f7a1432 --- /dev/null +++ b/javaTest/src/main/java/com/ft546/wcs/shadow/history/visualizer/SimulationVisualizationMapper.java @@ -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; + +/** + * 将调度仿真证据转换为前端可序列化的数据结构。 + * + *

该类不调用规划器,也不修改仿真结果。页面看到的车辆、路径和事件均来自同一次 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 bootstrap(HistoricalSimulationInput input) { + requireInput(input); + Map payload = new LinkedHashMap(); + 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 orderedSites = new ArrayList(input.getGridMap().getSites().values()); + Collections.sort(orderedSites, Comparator.comparing(Site::getId)); + List> sites = new ArrayList>(); + Set floors = new TreeSet(); + for (Site site : orderedSites) { + sites.add(site(site)); + floors.add(site.getFloor()); + } + payload.put("floors", new ArrayList(floors)); + payload.put("sites", sites); + + List orderedVehicles = + new ArrayList(input.getInitialVehicles()); + Collections.sort(orderedVehicles, Comparator.comparing(VehicleSnapshot::getVehicleId)); + List> vehicles = new ArrayList>(); + for (VehicleSnapshot vehicle : orderedVehicles) vehicles.add(vehicle(vehicle)); + payload.put("vehicles", vehicles); + return payload; + } + + /** 返回一次仿真的任务、事件、指标、归档和安全证据。 */ + public Map result(HistoricalSimulationInput input, + HistoricalDispatchSimulationResult result) { + requireInput(input); + if (result == null) throw new IllegalArgumentException("result is required"); + + Map definitions = taskDefinitions(input); + List orderedTasks = + new ArrayList(result.getTaskResults()); + Collections.sort(orderedTasks, + Comparator.comparing(SimulationTaskResult::getSimulationTaskId)); + + List> tasks = new ArrayList>(); + for (SimulationTaskResult task : orderedTasks) { + tasks.add(task(task, definitions.get(task.getSimulationTaskId()), + result.getAcceleration())); + } + List> events = new ArrayList>(); + for (SimulationTickEvent event : result.getEvents()) events.add(event(event)); + + Map payload = new LinkedHashMap(); + 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 summary(HistoricalDispatchSimulationResult result) { + Map value = new LinkedHashMap(); + 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 task(SimulationTaskResult task, + HistoricalSimulationTask definition, + int acceleration) { + HistoricalQueueRecord record = task.getRecord(); + List historicalPath = historicalPath(record.getPathInfo()); + Long historicalDuration = historicalDuration(task); + Long simulatedDuration = task.getCompletionMillis(); + Long scaledDuration = simulatedDuration == null + ? null : simulatedDuration * acceleration; + + Map value = new LinkedHashMap(); + 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 event(SimulationTickEvent event) { + Map value = new LinkedHashMap(); + 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 site(Site site) { + Map value = new LinkedHashMap(); + 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 vehicle(VehicleSnapshot vehicle) { + Map value = new LinkedHashMap(); + 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 invariants(SimulationInvariantSummary source) { + Map value = new LinkedHashMap(); + 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 archive(HistoricalDispatchSimulationResult result) { + Map value = new LinkedHashMap(); + 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 wms(HistoricalDispatchSimulationResult result) { + Map value = new LinkedHashMap(); + 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 timeStats(List samples) { + SimulationMetrics metrics = new SimulationMetrics(); + Map value = new LinkedHashMap(); + 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 waitingSamples(HistoricalDispatchSimulationResult result) { + List values = new ArrayList(); + for (SimulationTaskResult task : result.getTaskResults()) { + if (task.getWaitingMillis() != null) values.add(task.getWaitingMillis()); + } + return values; + } + + private List completionSamples(HistoricalDispatchSimulationResult result) { + List values = new ArrayList(); + for (SimulationTaskResult task : result.getTaskResults()) { + if (task.getCompletionMillis() != null) values.add(task.getCompletionMillis()); + } + return values; + } + + private List historicalSamples(HistoricalDispatchSimulationResult result) { + List values = new ArrayList(); + for (SimulationTaskResult task : result.getTaskResults()) { + Long duration = historicalDuration(task); + if (duration != null) values.add(duration); + } + return values; + } + + private List durationDifferenceSamples(HistoricalDispatchSimulationResult result) { + List values = new ArrayList(); + 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 aggregateReasons(HistoricalDispatchSimulationResult result, + boolean holds) { + Map values = new LinkedHashMap(); + for (SimulationTaskResult task : result.getTaskResults()) { + Map source = holds ? task.getHoldReasons() : task.getRetryReasons(); + for (Map.Entry entry : source.entrySet()) { + Integer previous = values.get(entry.getKey()); + values.put(entry.getKey(), (previous == null ? 0 : previous) + entry.getValue()); + } + } + return values; + } + + private Map failureReasons(HistoricalDispatchSimulationResult result) { + Map values = new LinkedHashMap(); + 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 historicalPath(String pathInfo) { + if (pathInfo == null || pathInfo.trim().isEmpty()) return Collections.emptyList(); + List values = new ArrayList(); + for (String node : Arrays.asList(pathInfo.split(","))) { + String normalized = node.trim(); + if (!normalized.isEmpty()) values.add(normalized); + } + return Collections.unmodifiableList(values); + } + + private Map taskDefinitions(HistoricalSimulationInput input) { + Map values = + new LinkedHashMap(); + 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"); + } +} diff --git a/javaTest/src/test/java/com/ft546/wcs/shadow/history/visualizer/SimulationVisualizationMapperTest.java b/javaTest/src/test/java/com/ft546/wcs/shadow/history/visualizer/SimulationVisualizationMapperTest.java new file mode 100644 index 0000000..40e5563 --- /dev/null +++ b/javaTest/src/test/java/com/ft546/wcs/shadow/history/visualizer/SimulationVisualizationMapperTest.java @@ -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 payload = mapper().bootstrap(input); + List> sites = listOfMaps(payload.get("sites")); + List> 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 payload = mapper().result(input, result); + List> tasks = listOfMaps(payload.get("tasks")); + List> events = listOfMaps(payload.get("events")); + Map summary = map(payload.get("summary")); + Map 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 summary = map(mapper().result(input, result).get("summary")); + Map waiting = map(summary.get("waitingTime")); + Map 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> listOfMaps(Object value) { + return (List>) value; + } + + @SuppressWarnings("unchecked") + private static Map map(Object value) { + return (Map) value; + } +}