Commit 1ade661a authored by Tong Li's avatar Tong Li

POA

parent e3a2cdde
package com.aps.demo;
import com.aps.entity.Algorithm.OperationDependency;
import com.aps.entity.Algorithm.OrderMaterialRequirement;
import com.aps.entity.basic.Entry;
import com.aps.entity.basic.Machine;
import com.aps.entity.basic.MachineOption;
import com.aps.poa.data.BomRelation;
import com.aps.poa.data.DataAdapter;
import com.aps.poa.data.InventoryConstraint;
import com.aps.poa.data.ModelConfig;
import com.aps.poa.data.OpDef;
import com.aps.poa.data.ResourceDef;
import com.aps.poa.data.SetupMatrix;
import com.aps.poa.data.SolveResult;
import com.aps.poa.model.POAOrToolsModel;
import com.google.ortools.Loader;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* POAOrToolsModel 可运行测试:
* 1. 业务实体版(Entry / Machine / materialRequirements 推导 BOM)
* 2. 内部模型版(OpDef / ResourceDef / 换型 / 时间窗 / 库存)
*/
class POAOrToolsModelTest {
@BeforeAll
static void init() {
Loader.loadNativeLibraries();
}
@Test
void testSolve_WithBusinessEntities() {
// ---- 1. 设备(Machine)----
List<Machine> machines = Arrays.asList(
new Machine(0, "M0"),
new Machine(1, "M1"),
new Machine(2, "M2")
);
// ---- 2. 工序(Entry):priority 设高 → 强制全规划 ----
List<Entry> entries = buildEntries(100);
// ---- 3. BOM:J0 首工序消耗 J1、J2 半成品(materialRequirements)----
entries.get(0).setMaterialRequirements(Arrays.asList(
bomReq("J1"), bomReq("J2")));
// ---- 4. 求解 ----
ModelConfig cfg = new ModelConfig();
cfg.timeLimitSeconds = 10;
cfg.weightMakespan = 1.0;
cfg.weightUnplanned = 1000.0;
POAOrToolsModel scheduler = new POAOrToolsModel(entries, machines, cfg);
scheduler.build();
SolveResult result = scheduler.solve();
printResult(result);
// ---- 5. 断言 ----
assertTrue(result.isFeasible, "模型应可行");
assertTrue(result.makespan > 0, "makespan 应大于 0");
assertTrue(result.unscheduledOps.isEmpty(),
"所有工序均应被规划,实际未规划: " + result.unscheduledOps);
assertEquals(7, result.startTimes.size(), "应规划 7 道工序");
// 前序约束(prevEntryIds 映射):end(prev) <= start(curr)
assertOrder("1", "2", result); // J0_O0 → J0_O1
assertOrder("2", "3", result); // J0_O1 → J0_O2
assertOrder("4", "5", result); // J1_O0 → J1_O1
assertOrder("6", "7", result); // J2_O0 → J2_O1
// BOM 齐套(materialRequirements 推导):父首工序 >= max(子末工序 end)
assertTrue(result.startTimes.get("1") >= Math.max(
result.endTimes.get("5"), result.endTimes.get("7")) - 1e-6,
"BOM 齐套约束应满足(J0 首工序应晚于 J1/J2 末工序完工)");
}
@Test
void testToOpDefs_EntryMapping() {
// 单工序:2 台候选设备 + 1 个前序依赖 + 优先级
List<Entry> entries = new ArrayList<>();
entries.add(op(101, "J0", 1, "成品-工序0", 5,
opt(0L, 12), opt(1L, 10)));
dep(entries.get(0), op(100, "J0", 0, "前序工序", 1, opt(0L, 8)));
List<OpDef> ops = DataAdapter.toOpDefs(entries);
assertEquals(1, ops.size(), "应转换出 1 道工序");
OpDef opDef = ops.get(0);
assertEquals("101", opDef.id, "工序 ID 应为 Entry.id 的字符串");
assertEquals("成品-工序0", opDef.name, "工序名称应取自 routingDetailName");
assertEquals(2, opDef.candidateResources.size(), "应保留 2 台候选设备");
assertEquals("0", opDef.candidateResources.get(0), "候选设备1 应为 machineId=0");
assertEquals("1", opDef.candidateResources.get(1), "候选设备2 应为 machineId=1");
assertEquals(12.0, opDef.durations.get(0), 1e-6, "设备0 加工时长应为 12");
assertEquals(10.0, opDef.durations.get(1), 1e-6, "设备1 加工时长应为 10");
assertEquals(5000.0, opDef.unplannedCost, 1e-6, "未规划惩罚应为 priority(5) * 1000");
assertEquals(1, opDef.predecessors.size(), "应保留 1 个前序依赖");
assertEquals(0.0, opDef.predecessors.get("100"), 1e-6, "前序依赖应为 prevOperationId=100");
}
@Test
void testToOpDefs_NoMachineOptions_Throws() {
Entry e = new Entry();
e.setId(1);
e.setOrderId("J0");
e.setSequence(1);
assertThrows(IllegalArgumentException.class,
() -> DataAdapter.toOpDefs(Arrays.asList(e)),
"工序没有候选设备时应抛出异常");
}
@Test
void testToOpDefs_MultiInputProducts() {
// FG1 首工序(id=1) 消耗 SUB1(5) + SUB2(3) 两种物料;SUB1/SUB2 末工序各自产出
List<Entry> entries = new ArrayList<>();
entries.add(setMr(op(1, "FG1", 1, "成品-工序0", 100, opt(0L, 6), opt(1L, 5)),
bomReq("SUB1", 5), bomReq("SUB2", 3)));
entries.add(op(11, "SUB1", 1, "半成品1-工序0", 100, opt(0L, 5)));
entries.add(setProduct(op(12, "SUB1", 2, "半成品1-工序1", 100, opt(1L, 7)), "SUB1", 5));
entries.add(op(13, "SUB2", 1, "半成品2-工序0", 100, opt(0L, 6)));
entries.add(setProduct(op(14, "SUB2", 2, "半成品2-工序1", 100, opt(1L, 7)), "SUB2", 3));
List<OpDef> ops = DataAdapter.toOpDefs(entries);
OpDef fg1o0 = ops.get(0);
// 多物料消耗:inputProducts 应包含两个条目(修复前的单值 inputQty 会覆盖丢失)
assertEquals(2, fg1o0.inputProducts.size(), "工序应消耗 2 种物料");
assertEquals(5.0, fg1o0.inputProducts.get("SUB1"), 1e-6, "应消耗 SUB1 数量 5");
assertEquals(3.0, fg1o0.inputProducts.get("SUB2"), 1e-6, "应消耗 SUB2 数量 3");
// 产出映射:SUB1/SUB2 末工序
OpDef sub1o1 = ops.get(2);
assertEquals(5.0, sub1o1.outputQty, 1e-6, "SUB1 末工序产出 5");
assertEquals("SUB1", sub1o1.productId, "SUB1 末工序产出产品 SUB1");
OpDef sub2o1 = ops.get(4);
assertEquals(3.0, sub2o1.outputQty, 1e-6, "SUB2 末工序产出 3");
assertEquals("SUB2", sub2o1.productId, "SUB2 末工序产出产品 SUB2");
}
@Test
void testToResourceDefs_MachineMapping() {
List<ResourceDef> res = DataAdapter.toResourceDefs(Arrays.asList(
new Machine(0, "M0"),
new Machine(5, "M5")));
assertEquals(2, res.size(), "应转换出 2 台设备");
assertEquals("0", res.get(0).id, "资源 ID 应为 Machine.id 的字符串");
assertEquals("5", res.get(1).id, "资源 ID 应为 Machine.id 的字符串");
}
@Test
void testToBomRelations_FromMaterialRequirements() {
List<Entry> entries = buildEntries(100);
// J0 首工序(id=1) 消耗 J1、J2 半成品 → 父首工序 ≥ 子末工序(id=5、7)
entries.get(0).setMaterialRequirements(Arrays.asList(
bomReq("J1"), bomReq("J2")));
List<BomRelation> boms = DataAdapter.toBomRelations(entries);
assertEquals(1, boms.size(), "应推导出 1 条 BOM 关系");
BomRelation bom = boms.get(0);
assertEquals("1", bom.parentFirstOpId, "父工序应为 J0 首工序(id=1)");
assertEquals(2, bom.childLastOpIds.size(), "应包含 2 个子工单末工序");
assertTrue(bom.childLastOpIds.contains("5"), "子末工序应包含 J1 末工序(id=5)");
assertTrue(bom.childLastOpIds.contains("7"), "子末工序应包含 J2 末工序(id=7)");
}
@Test
void testToBomRelations_NoMaterialRequirements_Empty() {
List<BomRelation> boms = DataAdapter.toBomRelations(buildEntries(100));
assertTrue(boms.isEmpty(), "无 materialRequirements 时 BOM 关系应为空");
}
@Test
void testSolve_WithOpDefs_AllConstraintsSatisfied() {
// ---- 1. 设备 ----
List<ResourceDef> resources = Arrays.asList(
new ResourceDef("M0", 500),
new ResourceDef("M1", 500),
new ResourceDef("M2", 500)
);
// ---- 2. 工序(FJSP:多设备可选)----
// unplannedCost 设高值,确保求解器强制所有工序规划(便于验证各约束)
List<OpDef> ops = new ArrayList<>();
ops.add(OpDef.builder("J0_O0", "成品-工序0")
.addResource("M0", 12).addResource("M1", 10)
.earliestStart(0).unplannedCost(100000)
.build());
ops.add(OpDef.builder("J0_O1", "成品-工序1")
.addResource("M1", 14).addResource("M2", 15)
.predecessor("J0_O0")
.unplannedCost(100000)
.build());
ops.add(OpDef.builder("J0_O2", "成品-工序2")
.addResource("M0", 8).addResource("M2", 11)
.predecessor("J0_O1")
.outputQty(1, "FG") // 成品产出,用于库存约束
.unplannedCost(100000)
.build());
ops.add(OpDef.builder("J1_O0", "半A-工序0")
.addResource("M0", 8).addResource("M2", 9)
.unplannedCost(100000)
.build());
ops.add(OpDef.builder("J1_O1", "半A-工序1")
.addResource("M1", 11)
.predecessor("J1_O0")
.unplannedCost(100000)
.build());
ops.add(OpDef.builder("J2_O0", "半B-工序0")
.addResource("M1", 7).addResource("M2", 10)
.unplannedCost(100000)
.build());
ops.add(OpDef.builder("J2_O1", "半B-工序1")
.addResource("M0", 13).addResource("M1", 9)
.predecessor("J2_O0")
.latestEnd(60) // 时间窗软约束:最晚 60 时刻完工
.unplannedCost(100000)
.build());
// ---- 3. BOM 齐套:成品首工序 ≥ 半成品末工序 ----
List<BomRelation> bom = Arrays.asList(
new BomRelation("J0_O0", Arrays.asList("J1_O1", "J2_O1"))
);
// ---- 4. 换型矩阵 ----
Map<String, Double> setupM0 = new HashMap<>();
setupM0.put("J0_O0|J2_O1", 3.0);
setupM0.put("J2_O1|J0_O0", 3.0);
setupM0.put("J0_O2|J1_O0", 2.0);
Map<String, Double> setupM1 = new HashMap<>();
setupM1.put("J0_O0|J0_O1", 4.0);
setupM1.put("J1_O1|J0_O1", 2.0);
List<SetupMatrix> setups = Arrays.asList(
new SetupMatrix("M0", setupM0),
new SetupMatrix("M1", setupM1)
);
// ---- 5. 库存约束(软约束)----
List<InventoryConstraint> inv = Arrays.asList(
new InventoryConstraint("FG", 100, 0, 1000, false)
);
// ---- 6. 配置 ----
ModelConfig cfg = new ModelConfig();
cfg.timeLimitSeconds = 10;
cfg.weightMakespan = 1.0;
cfg.weightUnplanned = 1000.0;
cfg.weightLatestEndViol = 500.0;
// ---- 7. 构建并求解 ----
POAOrToolsModel scheduler = new POAOrToolsModel(ops, resources, bom, setups, inv, cfg);
scheduler.build();
SolveResult result = scheduler.solve();
printResult(result);
// ---- 8. 断言 ----
assertTrue(result.isFeasible, "模型应可行");
assertTrue(result.makespan > 0, "makespan 应大于 0");
// 未规划惩罚权重高,7 道工序均应被规划
assertTrue(result.unscheduledOps.isEmpty(),
"所有工序均应被规划,实际未规划: " + result.unscheduledOps);
assertEquals(7, result.startTimes.size(), "应规划 7 道工序");
// 前序约束:end(prev) <= start(curr)
assertOrder("J0_O0", "J0_O1", result);
assertOrder("J0_O1", "J0_O2", result);
assertOrder("J1_O0", "J1_O1", result);
assertOrder("J2_O0", "J2_O1", result);
// BOM 齐套:父首工序 start >= max(子末工序 end)
assertTrue(result.startTimes.get("J0_O0") >= Math.max(
result.endTimes.get("J1_O1"), result.endTimes.get("J2_O1")) - 1e-6,
"BOM 齐套约束应满足");
// 时间窗软约束:J2_O1 最晚完工 60(允许极小容差)
assertTrue(result.endTimes.get("J2_O1") <= 60 + 1e-6,
"J2_O1 应满足最晚完工时间窗");
}
private void assertOrder(String prevOp, String currOp, SolveResult r) {
assertTrue(r.endTimes.get(prevOp) <= r.startTimes.get(currOp) + 1e-6,
prevOp + " 应早于 " + currOp + " 完成");
}
/**
* 构建 3 个工单(J0 成品 + J1/J2 半成品)共 7 道工序的 Entry 列表。
* 工序ID: 1-7;前序依赖通过 prevEntryIds 设置。
*/
private List<Entry> buildEntries(double priority) {
List<Entry> entries = new ArrayList<>();
entries.add(op(1, "J0", 1, "成品-工序0", priority, opt(0L, 12), opt(1L, 10)));
entries.add(op(2, "J0", 2, "成品-工序1", priority, opt(1L, 14), opt(2L, 15)));
entries.add(op(3, "J0", 3, "成品-工序2", priority, opt(0L, 8), opt(2L, 11)));
entries.add(op(4, "J1", 1, "半A-工序0", priority, opt(0L, 8), opt(2L, 9)));
entries.add(op(5, "J1", 2, "半A-工序1", priority, opt(1L, 11)));
entries.add(op(6, "J2", 1, "半B-工序0", priority, opt(1L, 7), opt(2L, 10)));
entries.add(op(7, "J2", 2, "半B-工序1", priority, opt(0L, 13), opt(1L, 9)));
dep(entries.get(1), entries.get(0)); // J0_O1 依赖 J0_O0
dep(entries.get(2), entries.get(1)); // J0_O2 依赖 J0_O1
dep(entries.get(4), entries.get(3)); // J1_O1 依赖 J1_O0
dep(entries.get(6), entries.get(5)); // J2_O1 依赖 J2_O0
return entries;
}
private Entry op(int id, String orderId, int sequence, String name, double priority,
MachineOption... opts) {
Entry e = new Entry();
e.setId(id);
e.setOrderId(orderId);
e.setSequence(sequence);
e.setRoutingDetailName(name);
e.setPriority(priority);
e.setMachineOptions(Arrays.asList(opts));
return e;
}
private MachineOption opt(Long machineId, double processingTime) {
MachineOption mo = new MachineOption();
mo.setMachineId(machineId);
mo.setProcessingTime(processingTime);
return mo;
}
private void dep(Entry entry, Entry prev) {
OperationDependency d = new OperationDependency();
d.setPrevOperationId(prev.getId());
entry.getPrevEntryIds().add(d);
}
/** BOM 物料需求:childOrderId 指向子工单 */
private OrderMaterialRequirement bomReq(String childOrderId) {
OrderMaterialRequirement omr = new OrderMaterialRequirement();
omr.setChildOrderId(childOrderId);
return omr;
}
/** BOM 物料需求:childOrderId 指向子工单 + 消耗数量 */
private OrderMaterialRequirement bomReq(String childOrderId, double qty) {
OrderMaterialRequirement omr = bomReq(childOrderId);
omr.setRequiredQuantity(qty);
return omr;
}
/** 设置工序的物料需求(BOM + 库存消耗) */
private Entry setMr(Entry e, OrderMaterialRequirement... reqs) {
e.setMaterialRequirements(Arrays.asList(reqs));
return e;
}
/** 设置工序的产品产出(库存产出) */
private Entry setProduct(Entry e, String productId, double qty) {
e.setProductId(productId);
e.setQuantity(qty);
return e;
}
private void printResult(SolveResult result) {
System.out.println("\n========== 排产结果 ==========");
System.out.println(result);
System.out.println("工序明细:");
List<Map.Entry<String, Double>> sorted = new ArrayList<>(result.startTimes.entrySet());
sorted.sort(Map.Entry.comparingByValue());
for (Map.Entry<String, Double> e : sorted) {
String opId = e.getKey();
System.out.printf(" %-8s → %s | start=%.1f, end=%.1f, dur=%.1f%n",
opId,
result.assignedResource.get(opId),
e.getValue(),
result.endTimes.get(opId),
result.endTimes.get(opId) - e.getValue());
}
if (!result.unscheduledOps.isEmpty()) {
System.out.println("未规划工序:" + result.unscheduledOps);
}
}
}
package com.aps.demo;
/**
* POAOrToolsModelTest 的独立运行入口(不依赖 JUnit 引擎 / maven surefire)。
*
* 运行方式(本项目根目录):
* javac -encoding UTF-8 -d target\demo-test-classes -cp <依赖classpath> ^
* src\main\java\com\aps\poa\model\POAOrToolsModel.java ^
* src\test\java\com\aps\demo\POAOrToolsModelTest.java ^
* src\test\java\com\aps\demo\POAOrToolsModelTestRunner.java
* java -cp <classpath> com.aps.demo.POAOrToolsModelTestRunner
*/
public class POAOrToolsModelTestRunner {
public static void main(String[] args) {
try {
POAOrToolsModelTest.init();
POAOrToolsModelTest test = new POAOrToolsModelTest();
test.testToOpDefs_EntryMapping();
test.testToOpDefs_NoMachineOptions_Throws();
test.testToOpDefs_MultiInputProducts();
test.testToResourceDefs_MachineMapping();
test.testToBomRelations_FromMaterialRequirements();
test.testToBomRelations_NoMaterialRequirements_Empty();
test.testSolve_WithBusinessEntities();
test.testSolve_WithOpDefs_AllConstraintsSatisfied();
System.out.println("\n[TEST PASS] 全部 8 个用例通过");
} catch (Throwable t) {
System.err.println("\n[TEST FAIL] " + t);
t.printStackTrace(System.err);
System.exit(1);
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment