feat: prepare verified history simulation inputs

This commit is contained in:
zgw
2026-07-31 11:18:49 +08:00
parent 8a3cf790c9
commit 78e74daea1
11 changed files with 395 additions and 11 deletions

View File

@@ -55,7 +55,7 @@ Expected: compilation fails because the new input types do not exist.
- [ ] **Step 3: Implement immutable map loading and task conversion**
Parse SHA-verified map details into an insertion-ordered immutable map. Map task types `1/2/3`, preserve raw business fields, assign ID `task_no@guid`, parse `create_date`, and retain `finish_date/agv_id/path_info` only in comparison fields.
Parse SHA-verified map details into an insertion-ordered immutable map. Map task types `1/2/3`, preserve raw business fields, assign stable per-row ID `task_no@guid#stableSequence` because the snapshot contains repeated task/GUID pairs, parse `create_date`, and retain `finish_date/agv_id/path_info` only in comparison fields.
- [ ] **Step 4: Build replannable topology and explicit endpoint mapping**

View File

@@ -23,7 +23,7 @@
## 3. 输入口径
- 使用已固化并经 SHA-256 校验的 240 条真实历史记录。
- 每条数据库行都是独立任务,唯一仿真 ID 为 `task_no@guid`;相同任务号的多阶段记录不合并。
- 每条数据库行都是独立任务。快照中 `task_no@guid` 只有 98 个唯一组合,因此唯一仿真 ID 为 `task_no@guid#stableSequence`;相同业务标识的多阶段或重复记录不合并。
- `create_date` 决定任务到达顺序和到达间隔。离散事件时钟支持 `10x`、`100x`,默认 `100x`;相同时间同时到达,超过 30 分钟的历史空闲间隔封顶为 18 仿真秒,避免测试等待数月。
- `task_type`:`1→INBOUND`、`2→OUTBOUND`、`3→RELOCATION`。
- `priority` 作为业务优先级;缺失时使用最低优先级。

View File

@@ -0,0 +1,26 @@
package com.ft546.wcs.shadow.history;
public final class HistoricalMapSite {
private final String siteId;
private final int x;
private final int y;
private final int z;
private final String axy;
public HistoricalMapSite(String siteId, int x, int y, int z, String axy) {
if (siteId == null || siteId.trim().isEmpty()) {
throw new IllegalArgumentException("site id must not be blank");
}
this.siteId = siteId;
this.x = x;
this.y = y;
this.z = z;
this.axy = axy == null ? "" : axy;
}
public String getSiteId() { return siteId; }
public int getX() { return x; }
public int getY() { return y; }
public int getZ() { return z; }
public String getAxy() { return axy; }
}

View File

@@ -3,12 +3,14 @@ package com.ft546.wcs.shadow.history;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public final class HistoricalQueueDataset {
private final List<HistoricalQueueRecord> records;
private final Set<String> mapSites;
private final Map<String, HistoricalMapSite> mapSiteDetails;
private final int manifestSampleRows;
private final int fullScanTotalRows;
private final String database;
@@ -22,7 +24,8 @@ public final class HistoricalQueueDataset {
private final int missingIntervalRows;
private final int reversedIntervalRows;
public HistoricalQueueDataset(List<HistoricalQueueRecord> records, Set<String> mapSites,
public HistoricalQueueDataset(List<HistoricalQueueRecord> records,
Map<String, HistoricalMapSite> mapSiteDetails,
int manifestSampleRows, int fullScanTotalRows,
String database, String sourceTable, String extractedAt,
String minCreateDate, String maxCreateDate,
@@ -30,7 +33,8 @@ public final class HistoricalQueueDataset {
int missingPathRows, int missingIntervalRows,
int reversedIntervalRows) {
this.records = Collections.unmodifiableList(new ArrayList<HistoricalQueueRecord>(records));
this.mapSites = Collections.unmodifiableSet(new LinkedHashSet<String>(mapSites));
this.mapSiteDetails = Collections.unmodifiableMap(
new LinkedHashMap<String, HistoricalMapSite>(mapSiteDetails));
this.manifestSampleRows = manifestSampleRows;
this.fullScanTotalRows = fullScanTotalRows;
this.database = database;
@@ -46,7 +50,11 @@ public final class HistoricalQueueDataset {
}
public List<HistoricalQueueRecord> getRecords() { return records; }
public Set<String> getMapSites() { return mapSites; }
public Set<String> getMapSites() {
return Collections.unmodifiableSet(
new LinkedHashSet<String>(mapSiteDetails.keySet()));
}
public Map<String, HistoricalMapSite> getMapSiteDetails() { return mapSiteDetails; }
public int getManifestSampleRows() { return manifestSampleRows; }
public int getFullScanTotalRows() { return fullScanTotalRows; }
public String getDatabase() { return database; }

View File

@@ -15,10 +15,9 @@ import java.nio.charset.CodingErrorAction;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.Map;
public final class HistoricalQueueLoader {
private static final ObjectMapper MAPPER = new ObjectMapper();
@@ -36,10 +35,19 @@ public final class HistoricalQueueLoader {
CollectionType rowsType = MAPPER.getTypeFactory()
.constructCollectionType(List.class, HistoricalQueueRecord.class);
List<HistoricalQueueRecord> rows = MAPPER.readValue(queueBytes, rowsType);
Set<String> sites = new LinkedHashSet<String>();
Map<String, HistoricalMapSite> sites =
new LinkedHashMap<String, HistoricalMapSite>();
for (JsonNode node : MAPPER.readTree(mapBytes)) {
String site = node.path("site_id").asText();
if (!site.isEmpty()) sites.add(site);
if (site.isEmpty()) {
throw new IllegalArgumentException("history map site id must not be blank");
}
HistoricalMapSite detail = new HistoricalMapSite(
site, node.path("x").asInt(), node.path("y").asInt(),
node.path("z").asInt(), node.path("axy").asText());
if (sites.put(site, detail) != null) {
throw new IllegalArgumentException("duplicate history map site " + site);
}
}
int sampleRows = manifest.path("sampleRows").asInt();
if (sampleRows != rows.size()) {

View File

@@ -0,0 +1,20 @@
package com.ft546.wcs.shadow.history.simulation;
import com.ft546.wcs.shadow.history.HistoricalQueueRecord;
public final class HistoricalSimulationFailure {
private final String simulationTaskId;
private final HistoricalQueueRecord record;
private final String reason;
HistoricalSimulationFailure(String simulationTaskId, HistoricalQueueRecord record,
String reason) {
this.simulationTaskId = simulationTaskId;
this.record = record;
this.reason = reason;
}
public String getSimulationTaskId() { return simulationTaskId; }
public HistoricalQueueRecord getRecord() { return record; }
public String getReason() { return reason; }
}

View File

@@ -0,0 +1,47 @@
package com.ft546.wcs.shadow.history.simulation;
import com.ft546.wcs.demo.model.VehicleSnapshot;
import com.ft546.wcs.demo.traffic.GridMap;
import com.ft546.wcs.shadow.map.EffectiveTrafficMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class HistoricalSimulationInput {
private final List<HistoricalSimulationTask> tasks;
private final List<HistoricalSimulationFailure> failedInputs;
private final GridMap gridMap;
private final EffectiveTrafficMap trafficMap;
private final List<VehicleSnapshot> initialVehicles;
HistoricalSimulationInput(List<HistoricalSimulationTask> tasks,
List<HistoricalSimulationFailure> failedInputs,
GridMap gridMap, EffectiveTrafficMap trafficMap,
List<VehicleSnapshot> initialVehicles) {
this.tasks = immutable(tasks);
this.failedInputs = immutable(failedInputs);
this.gridMap = gridMap;
this.trafficMap = trafficMap;
this.initialVehicles = immutable(initialVehicles);
}
public List<HistoricalSimulationTask> getTasks() { return tasks; }
public List<HistoricalSimulationFailure> getFailedInputs() { return failedInputs; }
public GridMap getGridMap() { return gridMap; }
public EffectiveTrafficMap getTrafficMap() { return trafficMap; }
public List<VehicleSnapshot> getInitialVehicles() { return initialVehicles; }
public List<String> allSimulationIds() {
List<String> ids = new ArrayList<String>();
for (HistoricalSimulationTask task : tasks) ids.add(task.getSimulationTaskId());
for (HistoricalSimulationFailure failure : failedInputs) {
ids.add(failure.getSimulationTaskId());
}
return Collections.unmodifiableList(ids);
}
private static <T> List<T> immutable(List<T> source) {
return Collections.unmodifiableList(new ArrayList<T>(source));
}
}

View File

@@ -0,0 +1,159 @@
package com.ft546.wcs.shadow.history.simulation;
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.traffic.GridMap;
import com.ft546.wcs.shadow.history.HistoricalMapSite;
import com.ft546.wcs.shadow.history.HistoricalPath;
import com.ft546.wcs.shadow.history.HistoricalQueueDataset;
import com.ft546.wcs.shadow.history.HistoricalQueueNormalizer;
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.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public final class HistoricalSimulationInputFactory {
public HistoricalSimulationInput create(HistoricalQueueDataset dataset) {
if (dataset == null) throw new IllegalArgumentException("dataset is required");
HistoricalQueueNormalizer normalizer = new HistoricalQueueNormalizer();
List<HistoricalPath> paths = new ArrayList<HistoricalPath>();
for (HistoricalQueueRecord record : dataset.getRecords()) {
paths.add(normalizer.normalize(record, dataset.getMapSites()));
}
Collections.sort(paths, Comparator
.comparing(HistoricalPath::getCreatedAt,
Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(path -> text(path.getRecord().getTaskNo()))
.thenComparing(path -> text(path.getRecord().getGuid())));
List<HistoricalSimulationTask> tasks = new ArrayList<HistoricalSimulationTask>();
List<HistoricalSimulationFailure> failures = new ArrayList<HistoricalSimulationFailure>();
int sequence = 0;
for (HistoricalPath path : paths) {
HistoricalQueueRecord record = path.getRecord();
int stableSequence = sequence++;
String id = simulationId(record, stableSequence);
DispatchTask.TaskType type = taskType(record.getTaskType());
if (!path.isReplayable() || type == null) {
failures.add(new HistoricalSimulationFailure(id, record, failureReason(path, type)));
continue;
}
List<String> nodes = path.getNodes();
String source = dataset.getMapSites().contains(record.getStartSite())
? record.getStartSite() : nodes.get(0);
String target = dataset.getMapSites().contains(record.getEndSite())
? record.getEndSite() : nodes.get(nodes.size() - 1);
tasks.add(new HistoricalSimulationTask(id, record, type,
record.getPriority() == null ? Integer.MAX_VALUE : record.getPriority(),
path.getCreatedAt(), path.getFinishedAt(), source, target, nodes, stableSequence));
}
GridMap gridMap = buildGridMap(tasks, dataset.getMapSiteDetails());
EffectiveTrafficMap trafficMap = buildTrafficMap(tasks);
List<VehicleSnapshot> vehicles = initialVehicles(tasks, dataset.getMapSiteDetails());
return new HistoricalSimulationInput(tasks, failures, gridMap, trafficMap, vehicles);
}
private GridMap buildGridMap(List<HistoricalSimulationTask> tasks,
Map<String, HistoricalMapSite> details) {
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>();
for (String id : ids) {
String site = earliest.get(id).getPhysicalSource();
vehicles.add(new VehicleSnapshot(id, details.get(site).getZ(), site,
5, 1, 100, true, true, false));
}
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) {
if ("1".equals(value)) return DispatchTask.TaskType.INBOUND;
if ("2".equals(value)) return DispatchTask.TaskType.OUTBOUND;
if ("3".equals(value)) return DispatchTask.TaskType.RELOCATION;
return null;
}
private String simulationId(HistoricalQueueRecord record, int 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; }
}

View File

@@ -0,0 +1,51 @@
package com.ft546.wcs.shadow.history.simulation;
import com.ft546.wcs.demo.model.DispatchTask;
import com.ft546.wcs.shadow.history.HistoricalQueueRecord;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class HistoricalSimulationTask {
private final String simulationTaskId;
private final HistoricalQueueRecord record;
private final DispatchTask.TaskType taskType;
private final int priority;
private final LocalDateTime createdAt;
private final LocalDateTime historicalFinishedAt;
private final String physicalSource;
private final String physicalTarget;
private final List<String> historicalNodes;
private final int arrivalSequence;
HistoricalSimulationTask(String simulationTaskId, HistoricalQueueRecord record,
DispatchTask.TaskType taskType, int priority,
LocalDateTime createdAt, LocalDateTime historicalFinishedAt,
String physicalSource, String physicalTarget,
List<String> historicalNodes, int arrivalSequence) {
this.simulationTaskId = simulationTaskId;
this.record = record;
this.taskType = taskType;
this.priority = priority;
this.createdAt = createdAt;
this.historicalFinishedAt = historicalFinishedAt;
this.physicalSource = physicalSource;
this.physicalTarget = physicalTarget;
this.historicalNodes = Collections.unmodifiableList(
new ArrayList<String>(historicalNodes));
this.arrivalSequence = arrivalSequence;
}
public String getSimulationTaskId() { return simulationTaskId; }
public HistoricalQueueRecord getRecord() { return record; }
public DispatchTask.TaskType getTaskType() { return taskType; }
public int getPriority() { return priority; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getHistoricalFinishedAt() { return historicalFinishedAt; }
public String getPhysicalSource() { return physicalSource; }
public String getPhysicalTarget() { return physicalTarget; }
public List<String> getHistoricalNodes() { return historicalNodes; }
public int getArrivalSequence() { return arrivalSequence; }
}

View File

@@ -2,6 +2,7 @@ package com.ft546.wcs.shadow;
import com.ft546.wcs.shadow.history.HistoricalQueueDataset;
import com.ft546.wcs.shadow.history.HistoricalQueueLoader;
import com.ft546.wcs.shadow.history.HistoricalMapSite;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
@@ -22,6 +23,12 @@ class HistoricalQueueLoaderTest {
assertFalse(data.getRecords().get(0).getTaskNo().isEmpty());
assertFalse(data.getRecords().get(0).getAgvId().isEmpty());
assertTrue(data.getMapSites().size() > 1000);
assertEquals(1129, data.getMapSiteDetails().size());
HistoricalMapSite site = data.getMapSiteDetails().get("010701");
assertEquals(15, site.getX());
assertEquals(1, site.getY());
assertEquals(1, site.getZ());
assertEquals("15,1", site.getAxy());
assertEquals("FT546-wcs", data.getDatabase());
assertEquals("dbo.wcs_srgv_queue_finish", data.getSourceTable());
assertEquals("2026-03-01 10:18:24", data.getMinCreateDate());

View File

@@ -0,0 +1,58 @@
package com.ft546.wcs.shadow.history.simulation;
import com.ft546.wcs.demo.model.DispatchTask;
import com.ft546.wcs.shadow.history.HistoricalQueueDataset;
import com.ft546.wcs.shadow.history.HistoricalQueueLoader;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.stream.Collectors;
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 HistoricalSimulationInputFactoryTest {
private static HistoricalSimulationInput input;
@BeforeAll
static void createInput() {
HistoricalQueueDataset dataset = new HistoricalQueueLoader().load(
HistoricalSimulationInputFactoryTest.class.getClassLoader(), "shadow/history");
input = new HistoricalSimulationInputFactory().create(dataset);
}
@Test
void convertsEveryHistoryRowToOneUniqueSchedulableOrFailedInput() {
assertEquals(238, input.getTasks().size());
assertEquals(2, input.getFailedInputs().size());
assertEquals(240, input.allSimulationIds().size());
assertEquals(240, input.allSimulationIds().stream().distinct().count());
assertTrue(input.getFailedInputs().stream()
.allMatch(failure -> "UNKNOWN_MAP".equals(failure.getReason())));
}
@Test
void preservesBusinessInputsWhileMappingPhysicalEndpointsExplicitly() {
HistoricalSimulationTask task = input.getTasks().stream()
.filter(item -> "tsj".equals(item.getRecord().getEndSite()))
.findFirst().get();
assertEquals("tsj", task.getRecord().getEndSite());
assertFalse("tsj".equals(task.getPhysicalTarget()));
assertEquals(task.getHistoricalNodes().get(task.getHistoricalNodes().size() - 1),
task.getPhysicalTarget());
assertFalse(task.getHistoricalNodes().isEmpty());
}
@Test
void buildsReplannableTopologyAndInitializesTheHistoricalFleet() {
assertEquals(EnumSet.allOf(DispatchTask.TaskType.class), input.getTasks().stream()
.map(HistoricalSimulationTask::getTaskType).collect(Collectors.toSet()));
assertTrue(input.getGridMap().getSites().size() > 100);
assertTrue(input.getTrafficMap().getEdges().size() > 100);
assertEquals(Arrays.asList("1", "2", "3"), input.getInitialVehicles().stream()
.map(vehicle -> vehicle.getVehicleId()).collect(Collectors.toList()));
}
}