Commit 78ed82da authored by Tong Li's avatar Tong Li

Merge remote-tracking branch 'origin/tl'

parents c6e3d8ce bf94442c
This source diff could not be displayed because it is too large. You can view the blob instead.
package com.aps.demo;
import com.aps.poa.data.BomItemDef;
import com.aps.poa.data.BomRelation;
import com.aps.poa.data.InventoryConstraint;
import com.aps.poa.data.InventoryPolicy;
import com.aps.poa.data.MaterialDef;
import com.aps.poa.data.ModelConfig;
import com.aps.poa.data.OpDef;
import com.aps.poa.data.OrderDef;
import com.aps.poa.data.ResourceDef;
import com.aps.poa.data.SetupMatrix;
import com.aps.poa.data.SolveResult;
import com.aps.poa.data.*;
import com.aps.poa.model.POAOrToolsModel;
import com.google.ortools.Loader;
import com.google.ortools.sat.CpSolver;
......@@ -19,7 +9,10 @@ import com.google.ortools.sat.IntVar;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 宏排产补充功能测试:最大等待时间 maxWait、WIP 缓冲区容量、时变库存+采购+生产数量、混合方向。
......@@ -29,11 +22,12 @@ public class POAMacroPlanTest {
public static void main(String[] args) {
Loader.loadNativeLibraries();
// testMaxWait();
// testWipCapacity();
// testNowLowerBound();
testHybrid();
// testMaterialInventory();
// testMaxWait();
// testWipCapacity();
// testNowLowerBound();
// testHybrid();
testMrpExplode();
//testMaterialInventory();
System.out.println("\n[PASS] 宏排产补充功能测试全部通过");
}
......@@ -161,6 +155,127 @@ public class POAMacroPlanTest {
System.out.printf("[PASS] HYBRID: J0后推(end=%.1f→交期100), J1正排(end=%.1f)%n", end0, end1);
}
/** MRP 展开:从成品 BOM 自动生成半成品订单(倒排)+ 原材料采购需求 */
static void testMrpExplode() {
Map<String, MaterialDef> materials = new HashMap<>();
materials.put("FG", new MaterialDef("FG", "FG", "成品", MaterialDef.MaterialType.FINISHED));
materials.put("SUB", new MaterialDef("SUB", "SUB", "半成品", MaterialDef.MaterialType.SEMI_FINISHED));
materials.put("SUB2", new MaterialDef("SUB2", "SUB2", "半成品", MaterialDef.MaterialType.SEMI_FINISHED));
materials.put("RAW", new MaterialDef("RAW", "RAW", "原料", MaterialDef.MaterialType.RAW));
materials.put("RAW2", new MaterialDef("RAW2", "RAW2", "原料", MaterialDef.MaterialType.RAW));
Map<String, InventoryPolicy> policies = new HashMap<>();
policies.put("FG", new InventoryPolicy("FG", 0, 0, 0, 100, 0, 10));
policies.put("SUB", new InventoryPolicy("SUB", 0, 0, 0, 100, 0, 10));
policies.put("SUB2", new InventoryPolicy("SUB2", 0, 0, 0, 100, 0, 10));
List<ScheduledReceipt> scheduledReceipts=new ArrayList<>();
ScheduledReceipt scheduledReceipt1=new ScheduledReceipt(200,10);
scheduledReceipts.add(scheduledReceipt1);
policies.put("RAW", new InventoryPolicy("RAW", 50, 40, 0, 100, scheduledReceipts, 5, 10, 1L));
policies.put("RAW2", new InventoryPolicy("RAW2", 0, 0, 0, 100, 0, 5, 10, 1L));
List<BomItemDef> bom = Arrays.asList(
new BomItemDef("FG", "SUB", 2.0, "FG_O0", 0),
// new BomItemDef("FG", "SUB2", 3.0, "FG_O0", 0),
// new BomItemDef("FG", "RAW2", 4.0, "FG_O0", 0),
new BomItemDef("SUB", "RAW", 1.0, "SUB_O0", 0));
// 自制物料工艺模板(成品 FG + 半成品 SUB):模板工序 id 是"工艺级"标识,生产批量/产能上限定义在工艺工序上
Map<String, List<OpDef>> routing = new HashMap<>();
routing.put("FG", Arrays.asList(
OpDef.builder("FG_O0", "成品工序").addResource("M0", 10)
.orderId("TEMPLATE").unplannedCost(100000)
.maxProductionQty(100).lotMultiplier(10).build()));
routing.put("SUB", Arrays.asList(
OpDef.builder("SUB_O0", "半成品工序").addResource("M0", 10)
.orderId("TEMPLATE").unplannedCost(100000)
.maxProductionQty(100).lotMultiplier(10).build()));
routing.put("SUB2", Arrays.asList(
OpDef.builder("SUB_O2", "半成品工序").addResource("M0", 10)
.orderId("TEMPLATE").unplannedCost(100000)
.maxProductionQty(100).lotMultiplier(10).build()));
// 成品订单需求:只指定物料+数量+交期+方向,工序由工艺模板实例化(firstOp/lastOp 先占位)
List<OrderDef> finishedOrderReqs = Arrays.asList(
new OrderDef("FG_ORDER", null, null, 0, 60,
"FG", 19, OrderDef.PlanDirection.FORWARD, null, null, 0),
new OrderDef("FG_ORDER2", null, null, 0, 60,
"FG", 20, OrderDef.PlanDirection.FORWARD, null, null, 0));
// 1. 实例化成品订单工序(实例工序 id = orderId_模板id),并填充 firstOp/lastOp
Map<String, Map<String, String>> orderOpIdMaps = new LinkedHashMap<>();
List<OpDef> allOps = new ArrayList<>();
List<OrderDef> finishedOrders = new ArrayList<>();
for (OrderDef req : finishedOrderReqs) {
Map<String, String> m = new LinkedHashMap<>();
List<OpDef> ops = instantiateOrderOps(req.materialId, req.orderId, routing, m);
allOps.addAll(ops);
orderOpIdMaps.put(req.orderId, m);
finishedOrders.add(new OrderDef(req.orderId, ops.get(0).id, ops.get(ops.size() - 1).id,
req.release, req.due, req.materialId, req.quantity, req.planDirection,
req.parentOrderId, req.parentOperationId, req.supplyLeadTime));
}
MrpResult mrp = MrpExploder.explode(finishedOrders, materials, policies, bom, routing);
// ---- 排程 + 优化:把 MRP 展开结果送入 CP-SAT 求解 ----
// 3. 汇总半成品工序/订单/映射(修正半成品 parentOperationId 为父订单实例工序 id)
allOps.addAll(mrp.generatedOps);
orderOpIdMaps.putAll(mrp.operationIdMaps);
List<OrderDef> allOrders = new ArrayList<>(finishedOrders);
for (OrderDef sub : mrp.generatedOrders) {
Map<String, String> parentMap = orderOpIdMaps.get(sub.parentOrderId);
String actualParentOp = parentMap != null && parentMap.containsKey(sub.parentOperationId)
? parentMap.get(sub.parentOperationId)
: sub.parentOperationId;
allOrders.add(new OrderDef(sub.orderId, sub.firstOpId, sub.lastOpId,
sub.release, sub.due, sub.materialId, sub.quantity, sub.planDirection,
sub.parentOrderId, actualParentOp, sub.supplyLeadTime));
}
// 4. 实例化 BOM:每条消耗项按"生产父物料的订单"拆分,consumeAtOperationId 指向该订单实例工序
List<BomItemDef> scheduleBom = instantiateBom(bom, allOrders, orderOpIdMaps);
List<ResourceDef> resources = Arrays.asList(new ResourceDef("M0", 200));
ModelConfig cfg = new ModelConfig();
cfg.maxHorizon = 60;
cfg.timeLimitSeconds = 30;
POAOrToolsModel scheduler = new POAOrToolsModel(
allOps, resources, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(),
allOrders, cfg,
new ArrayList<>(materials.values()),
new ArrayList<>(policies.values()),
scheduleBom);
scheduler.build();
CpSolver solver = new CpSolver();
solver.getParameters().setMaxTimeInSeconds(30);
CpSolverStatus status = solver.solve(scheduler.getRawModel());
if (status != CpSolverStatus.OPTIMAL && status != CpSolverStatus.FEASIBLE) {
throw new AssertionError("MRP 排程+优化求解失败: " + status);
}
// 通用产量校验:每个自制订单的生产量 >= 需求量
for (OrderDef o : allOrders) {
if (o.materialId == null) continue;
IntVar qv = scheduler.getProductionQtyVars().get(o.orderId);
if (qv == null) continue;
double actual = solver.value(qv) / (double) POAOrToolsModel.QUANTITY_SCALE;
if (actual < o.quantity - 1e-6) {
throw new AssertionError(String.format(
"订单 %s 产量不足: %.2f < %.2f", o.orderId, actual, o.quantity));
}
}
printMacroPlan(scheduler, solver);
}
/** 时变库存 + 采购 + 生产数量 + 混合方向(成品正排 + 半成品倒排) */
static void testMaterialInventory() {
// 物料
......@@ -180,17 +295,19 @@ public class POAMacroPlanTest {
new BomItemDef("FG", "SUB", 2.0, "FG_O0", 0),
new BomItemDef("SUB", "RAW", 1.0, "SUB_O0", 0));
// 工序:FG 与 SUB 各一道,均在 M0
// 工序:FG 与 SUB 各一道,均在 M0;生产批量/产能上限定义在工艺工序上
List<OpDef> ops = Arrays.asList(
OpDef.builder("FG_O0", "成品工序").addResource("M0", 10).orderId("FG_ORDER").unplannedCost(100000).build(),
OpDef.builder("SUB_O0", "半成品工序").addResource("M0", 10).orderId("SUB_ORDER").unplannedCost(100000).build());
OpDef.builder("FG_O0", "成品工序").addResource("M0", 10).orderId("FG_ORDER").unplannedCost(100000)
.maxProductionQty(100).lotMultiplier(10).build(),
OpDef.builder("SUB_O0", "半成品工序").addResource("M0", 10).orderId("SUB_ORDER").unplannedCost(100000)
.maxProductionQty(100).lotMultiplier(10).build());
// 订单:FG 正排生产 10(release=40,给半成品留出生产时间);SUB 倒排生产 20,父订单 FG_ORDER,在 FG_O0 开工前 0 分钟到位
List<OrderDef> orders = Arrays.asList(
new OrderDef("FG_ORDER", "FG_O0", "FG_O0", 40, 60,
"FG", 10, 10, 10, 10, OrderDef.PlanDirection.FORWARD, null, null, 0),
"FG", 10, OrderDef.PlanDirection.FORWARD, null, null, 0),
new OrderDef("SUB_ORDER", "SUB_O0", "SUB_O0", -1, 60,
"SUB", 20, 20, 20, 10, OrderDef.PlanDirection.BACKWARD,
"SUB", 20, OrderDef.PlanDirection.BACKWARD,
"FG_ORDER", "FG_O0", 0));
List<ResourceDef> resources = Arrays.asList(new ResourceDef("M0", 200));
......@@ -223,5 +340,142 @@ public class POAMacroPlanTest {
if (subQty < 20 * POAOrToolsModel.QUANTITY_SCALE) {
throw new AssertionError("SUB 生产量应 >= 20,实际=" + subQty);
}
printMacroPlan(scheduler, solver);
}
/** 打印宏排产结果:成品/半成品订单工序的起止、产量、设备,以及原材料采购需求计划 */
static void printMacroPlan(POAOrToolsModel scheduler, CpSolver solver) {
System.out.println("\n========== 宏排产计划 ==========");
System.out.println("---- 订单工序计划(成品/半成品) ----");
Map<String, MaterialDef> materials = scheduler.getMaterials();
for (OrderDef order : scheduler.getOrderDefs()) {
MaterialDef mat = order.materialId == null ? null : materials.get(order.materialId);
String matLabel = mat == null ? order.materialId
: mat.name + "(" + typeLabel(mat.type) + ")";
double qty = 0;
if (order.materialId != null && scheduler.getProductionQtyVars().containsKey(order.orderId)) {
qty = solver.value(scheduler.getProductionQtyVars().get(order.orderId))
/ (double) POAOrToolsModel.QUANTITY_SCALE;
}
System.out.printf("订单 %-10s 物料=%-14s 方向=%-8s 生产数量=%.2f 交期=%.1f 父订单 %-10s%n",
order.orderId, matLabel, order.planDirection, qty, order.due,order.parentOrderId);
for (OpDef op : scheduler.getOps().values()) {
if (!order.orderId.equals(op.orderId)) continue;
double start = solver.value(scheduler.getStartVars().get(op.id))
/ (double) POAOrToolsModel.SCALE;
double end = solver.value(scheduler.getEndVars().get(op.id))
/ (double) POAOrToolsModel.SCALE;
String machine = "-";
for (String resId : op.candidateResources) {
if (solver.booleanValue(scheduler.getAssignVars().get(op.id + "_" + resId))) {
machine = resId;
break;
}
}
System.out.printf(" 工序 %-10s %-8s 开始=%6.1f 结束=%6.1f 设备=%s%n",
op.id, op.name, start, end, machine);
}
}
System.out.println("---- 原材料采购需求计划 ----");
boolean any = false;
for (Map.Entry<String, IntVar[]> e : scheduler.getPurchaseReceiptVars().entrySet()) {
String matId = e.getKey();
MaterialDef mat = materials.get(matId);
if (mat == null || (mat.type != MaterialDef.MaterialType.RAW
&& mat.type != MaterialDef.MaterialType.PURCHASED)) {
continue;
}
IntVar[] receipts = e.getValue();
IntVar[] lots = scheduler.getPurchaseLotCountVars().get(matId);
double total = 0;
List<String> buckets = new ArrayList<>();
for (int t = 0; t < receipts.length; t++) {
long q = solver.value(receipts[t]);
if (q <= 0) continue;
double qty = q / (double) POAOrToolsModel.QUANTITY_SCALE;
total += qty;
long lot = lots == null ? 0 : solver.value(lots[t]);
buckets.add(String.format("t=%d(+%.2f,%d批)", t, qty, lot));
}
if (total > 0 || !buckets.isEmpty()) {
any = true;
System.out.printf("物料 %-14s 采购总量=%.2f 到货批次: %s%n",
mat.name + "(" + matId + ")", total,
buckets.isEmpty() ? "无" : String.join(" ", buckets));
}
}
if (!any) {
System.out.println("(无采购需求)");
}
}
/** 从工艺模板实例化某订单的工序:工序 id 保持模板 id,仅绑定到订单 */
/** 从工艺模板实例化某订单的工序:实例工序 id = orderId_模板id,并把模板→实例映射写入 idMapOut */
static List<OpDef> instantiateOrderOps(String materialId, String orderId,
Map<String, List<OpDef>> routing,
Map<String, String> idMapOut) {
List<OpDef> templates = routing.get(materialId);
if (templates == null || templates.isEmpty()) {
throw new IllegalArgumentException("缺少工艺模板: " + materialId);
}
List<OpDef> ops = new ArrayList<>();
for (OpDef t : templates) {
String newId = orderId + "_" + t.id;
idMapOut.put(t.id, newId);
OpDef.Builder b = OpDef.builder(newId, t.name);
for (int i = 0; i < t.candidateResources.size(); i++) {
b.addResource(t.candidateResources.get(i), t.durations.get(i));
}
b.orderId(orderId)
.interruptible(t.interruptible)
.setupTime(t.setupTime)
.maxWaitAfter(t.maxWaitAfter)
.unplannedCost(t.unplannedCost)
.minProductionQty(t.minProductionQty)
.maxProductionQty(t.maxProductionQty)
.lotMultiplier(t.lotMultiplier);
if (t.bufferId != null) {
b.buffer(t.bufferId, t.bufferCapacity);
}
for (String prevId : t.predecessors.keySet()) {
String newPrev = idMapOut.get(prevId);
if (newPrev != null) b.predecessor(newPrev);
}
ops.add(b.build());
}
return ops;
}
/** 实例化 BOM:每条消耗项按"生产父物料的订单"拆分,consumeAtOperationId 指向该订单实例工序 */
static List<BomItemDef> instantiateBom(List<BomItemDef> bom,
List<OrderDef> allOrders,
Map<String, Map<String, String>> orderOpIdMaps) {
List<BomItemDef> out = new ArrayList<>();
for (BomItemDef item : bom) {
for (OrderDef o : allOrders) {
if (!item.parentMaterialId.equals(o.materialId)) continue;
Map<String, String> m = orderOpIdMaps.get(o.orderId);
String actualOp = m != null
? m.getOrDefault(item.consumeAtOperationId, item.consumeAtOperationId)
: item.consumeAtOperationId;
out.add(new BomItemDef(item.parentMaterialId, item.componentMaterialId,
item.qtyPer, actualOp, item.supplyLeadTime));
}
}
return out;
}
static String typeLabel(MaterialDef.MaterialType type) {
switch (type) {
case FINISHED: return "成品";
case SEMI_FINISHED: return "半成品";
case RAW: return "原材料";
case PURCHASED: return "采购件";
default: return type.name();
}
}
}
......@@ -81,6 +81,28 @@ public class Entry {
* 数量
*/
private double quantity;
/**
* 标准批量(工艺路线工序 batchQty),作为订单拆分时的每批目标大小
*/
private Double batchQty;
/**
* 拆分最小量(拆分批数量的下界)
*/
private Double splitMinQty;
/**
* 拆分最大量(拆分批数量的上界)
*/
private Double splitMaxQty;
/**
* 最大生产量(超过则触发拆分)
*/
private Double maxProductionQty;
/**
* 订单是否可拆分
*/
private boolean canSplit = false;
/**
* 工序顺序
*/
......@@ -113,10 +135,10 @@ public class Entry {
*/
private Integer state ;
/**
* 是否可中断,间缝插针
*/
private Long isInterrupt = 1l;
/**
* 是否可中断,间缝插针
*/
private Long isInterrupt = 1l;
/**
* 所需物料
......
......@@ -3,7 +3,9 @@ package com.aps.poa.constraint;
import com.aps.poa.data.BomItemDef;
import com.aps.poa.data.InventoryPolicy;
import com.aps.poa.data.MaterialDef;
import com.aps.poa.data.OpDef;
import com.aps.poa.data.OrderDef;
import com.aps.poa.data.ScheduledReceipt;
import com.aps.poa.model.POAOrToolsModel;
import com.google.ortools.sat.BoolVar;
import com.google.ortools.sat.CpModel;
......@@ -54,22 +56,22 @@ public final class MaterialInventoryConstraint {
int horizon = (int) model.getConfig().maxHorizon;
long qs = POAOrToolsModel.QUANTITY_SCALE;
// 生产数量 / 批数决策变量(仅 materialId 非空的订单)
// 生产数量 / 批数决策变量(仅 materialId 非空的订单),并缓存各订单最大生产量
Map<String, Long> orderMaxProduction = new HashMap<>();
for (OrderDef order : model.getOrderDefs()) {
if (order.materialId == null) continue;
long minQ = model.qtyToInt(order.minProductionQty);
long maxQ = Math.max(minQ, model.qtyToInt(order.maxProductionQty));
long lot = Math.max(1L, model.qtyToInt(order.productionLotSize));
OpDef op = productionOpOf(model, order);
long demand = model.qtyToInt(order.quantity);
long lot = Math.max(1L, op != null ? model.qtyToInt(op.lotMultiplier) : 1);
long minQ = Math.max(demand, op != null ? model.qtyToInt(op.minProductionQty) : 0);
minQ = ((minQ + lot - 1) / lot) * lot; // 向上取整到批量整数倍,避免 minQ 不是 lot 倍数导致不可行
long maxQ = Math.max(minQ, op != null ? model.qtyToInt(op.maxProductionQty) : 0);
IntVar qty = cm.newIntVar(minQ, maxQ, "prodQty_" + order.orderId);
IntVar lotCount = cm.newIntVar(0, maxQ / lot, "prodLots_" + order.orderId);
model.getProductionQtyVars().put(order.orderId, qty);
model.getProductionLotCountVars().put(order.orderId, lotCount);
cm.addEquality(qty, LinearExpr.term(lotCount, lot));
}
Map<String, List<BomItemDef>> bomByParent = new HashMap<>();
for (BomItemDef item : model.getBomItems()) {
bomByParent.computeIfAbsent(item.parentMaterialId, k -> new ArrayList<>()).add(item);
orderMaxProduction.put(order.orderId, maxQ);
}
List<IntVar> allSafetyShortfalls = new ArrayList<>();
......@@ -85,10 +87,19 @@ public final class MaterialInventoryConstraint {
long safety = model.qtyToInt(policy.safetyStock);
long max = model.qtyToInt(policy.maxStock);
long initial = model.qtyToInt(policy.onHand);
long scheduled = model.qtyToInt(policy.scheduledReceipts);
// 在途到货按 arrivalMinute 分桶(clamp 到 [0, horizon]),逐桶计入库存流入
Map<Integer, Long> arrivalByBucket = new HashMap<>();
for (ScheduledReceipt sr : policy.scheduledReceipts) {
int b = (int) Math.max(0L, Math.min(horizon, Math.round(sr.arrivalMinute)));
arrivalByBucket.merge(b, model.qtyToInt(sr.quantity), Long::sum);
}
totalSafetyCap += safety * (horizon + 1L);
totalHoldingCap += Math.max(1L, max) * (horizon + 1L) * Math.max(1L, policy.holdingCostPerUnitPerBucket);
if (material.type != MaterialDef.MaterialType.FINISHED) {
totalHoldingCap += Math.max(1L, max) * (horizon + 1L)
* Math.max(1L, policy.holdingCostPerUnitPerBucket);
}
List<IntVar[]> productionEvents = new ArrayList<>();
List<IntVar[]> consumptionEvents = new ArrayList<>();
......@@ -100,7 +111,7 @@ public final class MaterialInventoryConstraint {
IntVar jobEndMinute = minuteVar(model, order.lastOpId, false);
BoolVar[] endAt = createEventIndicators(cm, jobEndMinute, horizon,
"EVT_" + material.id + "_JOB_" + order.orderId + "_END");
long maxQ = model.qtyToInt(order.maxProductionQty);
long maxQ = orderMaxProduction.get(order.orderId);
IntVar[] qtyAt = new IntVar[horizon + 1];
for (int t = 0; t <= horizon; t++) {
qtyAt[t] = createQuantityEvent(cm, qty, endAt[t], maxQ,
......@@ -110,36 +121,38 @@ public final class MaterialInventoryConstraint {
}
// BOM 消耗:Consumption[t] = ParentQty × BOM比例 × [OperationStart=t]
for (OrderDef order : model.getOrderDefs()) {
if (order.materialId == null) continue;
IntVar parentQty = model.getProductionQtyVars().get(order.orderId);
// 每条 BOM 项通过 consumeAtOperationId 唯一定位到父订单的实例工序(支持同物料多订单)
for (BomItemDef bom : model.getBomItems()) {
if (!bom.componentMaterialId.equals(material.id)) continue;
OpDef parentOp = model.getOps().get(bom.consumeAtOperationId);
if (parentOp == null) continue;
String parentOrderId = parentOp.orderId;
IntVar parentQty = model.getProductionQtyVars().get(parentOrderId);
if (parentQty == null) continue;
for (BomItemDef bom : bomByParent.getOrDefault(order.materialId, new ArrayList<>())) {
if (!bom.componentMaterialId.equals(material.id)) continue;
IntVar opStartMinute = minuteVar(model, bom.consumeAtOperationId, true);
BoolVar[] startAt = createEventIndicators(cm, opStartMinute, horizon,
"EVT_" + material.id + "_JOB_" + order.orderId + "_OP_" + bom.consumeAtOperationId + "_START");
long maxParentQ = model.qtyToInt(order.maxProductionQty);
long bomNum = Math.max(1L, Math.round(bom.qtyPer * qs));
long maxConsumption = (maxParentQ * bomNum + qs - 1) / qs;
IntVar[] qtyAt = new IntVar[horizon + 1];
for (int t = 0; t <= horizon; t++) {
IntVar parentAtEvent = createQuantityEvent(cm, parentQty, startAt[t], maxParentQ,
"MAT_" + material.id + "_JOB_" + order.orderId + "_PARENT_AT_" + t);
qtyAt[t] = cm.newIntVar(0, maxConsumption,
"MAT_" + material.id + "_JOB_" + order.orderId + "_CONS_AT_" + t);
// qtyAt × qs = parentAtEvent × bomNum(线性化小数 BOM)
cm.addEquality(
LinearExpr.term(qtyAt[t], qs),
LinearExpr.term(parentAtEvent, bomNum));
}
consumptionEvents.add(qtyAt);
IntVar opStartMinute = minuteVar(model, bom.consumeAtOperationId, true);
BoolVar[] startAt = createEventIndicators(cm, opStartMinute, horizon,
"EVT_" + material.id + "_JOB_" + parentOrderId + "_OP_" + bom.consumeAtOperationId + "_START");
long maxParentQ = orderMaxProduction.get(parentOrderId);
long bomNum = Math.max(1L, Math.round(bom.qtyPer * qs));
long maxConsumption = (maxParentQ * bomNum + qs - 1) / qs;
IntVar[] qtyAt = new IntVar[horizon + 1];
for (int t = 0; t <= horizon; t++) {
IntVar parentAtEvent = createQuantityEvent(cm, parentQty, startAt[t], maxParentQ,
"MAT_" + material.id + "_JOB_" + parentOrderId + "_PARENT_AT_" + t);
qtyAt[t] = cm.newIntVar(0, maxConsumption,
"MAT_" + material.id + "_JOB_" + parentOrderId + "_CONS_AT_" + t);
cm.addEquality(
LinearExpr.term(qtyAt[t], qs),
LinearExpr.term(parentAtEvent, bomNum));
}
consumptionEvents.add(qtyAt);
}
IntVar[] stock = new IntVar[horizon + 1];
IntVar[] purchaseReceipts = new IntVar[horizon + 1];
IntVar[] purchaseLotsArr = new IntVar[horizon + 1];
for (int t = 0; t <= horizon; t++) {
stock[t] = cm.newIntVar(min, max, "INV_" + material.id + "_T" + t);
......@@ -149,6 +162,8 @@ public final class MaterialInventoryConstraint {
"PURCHASE_LOTS_" + material.id + "_T" + t);
IntVar purchaseReceipt = cm.newIntVar(0, lotSize * policy.maxPurchaseLotsPerBucket,
"PURCHASE_RECEIPT_" + material.id + "_T" + t);
purchaseLotsArr[t] = purchaseLots;
purchaseReceipts[t] = purchaseReceipt;
cm.addEquality(purchaseReceipt, LinearExpr.term(purchaseLots, lotSize));
if (material.type != MaterialDef.MaterialType.RAW
&& material.type != MaterialDef.MaterialType.PURCHASED) {
......@@ -179,7 +194,7 @@ public final class MaterialInventoryConstraint {
// 库存平衡
LinearExpr inflow = LinearExpr.sum(new LinearArgument[]{
LinearExpr.constant(t == 0 ? initial + scheduled : 0),
LinearExpr.constant((t == 0 ? initial : 0L) + arrivalByBucket.getOrDefault(t, 0L)),
purchaseReceipt,
prodReceipt,
LinearExpr.term(consumption, -1)});
......@@ -194,8 +209,9 @@ public final class MaterialInventoryConstraint {
cm.addGreaterOrEqual(shortfall, LinearExpr.sum(new LinearArgument[]{
LinearExpr.constant(safety), LinearExpr.term(stock[t], -1)}));
// 持有成本
if (policy.holdingCostPerUnitPerBucket > 0) {
// 持有成本(成品即产出,不计库存持有成本)
if (policy.holdingCostPerUnitPerBucket > 0
&& material.type != MaterialDef.MaterialType.FINISHED) {
IntVar holding = cm.newIntVar(0, max * policy.holdingCostPerUnitPerBucket,
"HOLDING_COST_" + material.id + "_T" + t);
cm.addEquality(holding, LinearExpr.term(stock[t], policy.holdingCostPerUnitPerBucket));
......@@ -203,6 +219,8 @@ public final class MaterialInventoryConstraint {
}
}
model.getInventoryVars().put(material.id, stock);
model.getPurchaseReceiptVars().put(material.id, purchaseReceipts);
model.getPurchaseLotCountVars().put(material.id, purchaseLotsArr);
}
IntVar totalShortfall = cm.newIntVar(0, totalSafetyCap, "total_safety_stock_shortfall");
......@@ -221,6 +239,11 @@ public final class MaterialInventoryConstraint {
model.getInventoryVars().size(), horizon + 1, model.getProductionQtyVars().size());
}
/** 取订单产出工序(lastOpId)上定义的工艺级生产参数 */
private static OpDef productionOpOf(POAOrToolsModel model, OrderDef order) {
return model.getOps().get(order.lastOpId);
}
/** 取工序的分钟级 start/end 变量(floor(scaled/SCALE),缓存复用) */
private static IntVar minuteVar(POAOrToolsModel model, String opId, boolean isStart) {
IntVar scaled = isStart ? model.getStartVars().get(opId) : model.getEndVars().get(opId);
......
package com.aps.poa.data;
import com.aps.entity.Algorithm.OperationDependency;
import com.aps.entity.Algorithm.OrderMaterialRequirement;
import com.aps.entity.basic.Entry;
import com.aps.entity.basic.MachineOption;
import org.springframework.beans.BeanUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 订单级标准批量拆分器(POA CP-SAT 前置处理)。
*
* <p>拆分规则:</p>
* <pre>
* 触发(全部满足):
* canSplit == true
* && quantity > maxProductionQty // 超过最大生产量
* && quantity > batchQty // 超过标准批量
*
* 每批目标大小 = batchQty,clamp 到 [splitMinQty, splitMaxQty];
* 批次数 N = ceil(quantity / 目标大小),前 N-1 批 = 目标大小,尾批 = 余量;
* 尾批 &lt; splitMinQty 时并入前一批(合并后仍 ≤ splitMaxQty)。
* </pre>
*
* <p>每批复制整条工艺路线生成子订单(子 orderId = 原orderId_b1/_b2…),
* 加工时长与 BOM 消耗数量按 批数量/原订单数量 比例缩放(暂不考虑 productionTakt)。</p>
*/
public final class BatchSplitter {
private BatchSplitter() {
}
/**
* 计算订单拆分后的每批数量。
*
* @param quantity 订单生产数量
* @param batchQty 标准批量(每批目标大小)
* @param splitMinQty 拆分最小量
* @param splitMaxQty 拆分最大量
* @param maxProductionQty 最大生产量
* @param canSplit 订单是否可拆分
* @return 每批数量列表;不拆分时返回单元素列表 [quantity]
*/
public static List<Double> splitQuantities(double quantity, Double batchQty,
Double splitMinQty, Double splitMaxQty,
Double maxProductionQty, boolean canSplit) {
double bq = nz(batchQty);//标准批量
double smin = nz(splitMinQty);//拆分最小量
double smax = nz(splitMaxQty);//拆分最大量
double mpq = nz(maxProductionQty);//最大生产量
// 不拆分:不可拆 / 未超过最大生产量 / 未超过标准批量
if (!canSplit || quantity <= mpq) {
return Collections.singletonList(quantity);
}
// 每批目标大小,clamp 到 [splitMinQty, splitMaxQty]
double target = bq;
if (smin > 0 && target < smin) target = smin;
if (smax > 0 && target > smax) target = smax;
if (target <= 0) {
return Collections.singletonList(quantity);
}
int n = (int) Math.ceil(quantity / target);
if (n <= 1) {
return Collections.singletonList(quantity);
}
List<Double> batches = new ArrayList<>();
double remaining = quantity;
for (int i = 0; i < n - 1; i++) {
batches.add(target);
remaining -= target;
}
double tail = remaining; // ∈ (0, target]
if (smin > 0 && tail < smin) {
// 尾批不足拆分最小量:并入前一批(合并后仍 ≤ splitMaxQty)
double lastFull = batches.get(batches.size() - 1);
if (smax <= 0 || lastFull + tail <= smax) {
batches.set(batches.size() - 1, lastFull + tail);
} else {
batches.add(tail);
}
} else {
batches.add(tail);
}
return batches;
}
/**
* 对订单列表做标准批量拆分:按 orderId 分组,对满足拆分条件的订单复制整条工艺路线,
* 生成多个子订单;不满足条件的订单原样返回。
*
* @param entries 原始工序列表(每道工序对应一个 Entry)
* @return 拆分后的工序列表(子订单的 orderId 后缀 _b1/_b2…,state=1,splitSourceId 指向原始工序 id)
*/
public static List<Entry> split(List<Entry> entries) {
if (entries == null || entries.isEmpty()) {
return entries;
}
Map<String, List<Entry>> byOrder = entries.stream()
.filter(e -> e.getOrderId() != null && !e.getOrderId().isEmpty())
.collect(Collectors.groupingBy(Entry::getOrderId, LinkedHashMap::new, Collectors.toList()));
// 拆分产生的新工序 id 从现有最大 id 之后开始,避免冲突
int nextId = entries.stream().mapToInt(Entry::getId).max().orElse(0) + 1;
List<Entry> result = new ArrayList<>();
Set<String> processed = new HashSet<>();
for (Entry e : entries) {
String orderId = e.getOrderId();
if (orderId == null || orderId.isEmpty()) {
result.add(e);
continue;
}
if (processed.contains(orderId)) {
continue;
}
processed.add(orderId);
List<Entry> orderOps = byOrder.get(orderId);
orderOps.sort((a, b) -> Integer.compare(a.getSequence(), b.getSequence()));
// 订单数量取各工序数量中的最大值(末工序产出量)
double quantity = orderOps.stream()
.mapToDouble(Entry::getQuantity)
.filter(q -> q > 0)
.max()
.orElse(0);
Entry first = orderOps.get(0);
List<Double> batches = splitQuantities(quantity,
first.getBatchQty(), first.getSplitMinQty(), first.getSplitMaxQty(),
first.getMaxProductionQty(), first.isCanSplit());
if (batches.size() <= 1) {
result.addAll(orderOps);
continue;
}
for (int b = 0; b < batches.size(); b++) {
double batchQty = batches.get(b);
String subOrderId = orderId + "_b" + (b + 1);
double scale = quantity > 0 ? batchQty / quantity : 1;
// 原工序 id → 新工序 id
Map<Integer, Integer> idMap = new HashMap<>();
for (Entry op : orderOps) {
idMap.put(op.getId(), nextId++);
}
for (Entry op : orderOps) {
result.add(cloneForBatch(op, idMap.get(op.getId()), subOrderId, batchQty, scale, idMap));
}
}
}
return result;
}
private static Entry cloneForBatch(Entry src, int newId, String subOrderId,
double batchQty, double scale, Map<Integer, Integer> idMap) {
Entry c = new Entry();
BeanUtils.copyProperties(src, c);
c.setId(newId);
c.setOrderId(subOrderId);
c.setQuantity(batchQty);
c.setState(1); // 拆分
c.setSplitSourceId(src.getId());
c.setMinProcessingTime(src.getMinProcessingTime() * scale);
c.setPrevEntryIds(remapDeps(src.getPrevEntryIds(), idMap));
c.setNextEntryIds(remapDeps(src.getNextEntryIds(), idMap));
c.setMachineOptions(scaleMachineOptions(src.getMachineOptions(), scale));
c.setMaterialRequirements(scaleMaterialRequirements(src.getMaterialRequirements(), scale));
return c;
}
/** 前/后序依赖重映射:仅重映射当前订单内部工序的 id,跨订单依赖保持原样 */
private static List<OperationDependency> remapDeps(List<OperationDependency> deps, Map<Integer, Integer> idMap) {
if (deps == null || deps.isEmpty()) {
return new ArrayList<>();
}
List<OperationDependency> out = new ArrayList<>();
for (OperationDependency d : deps) {
OperationDependency nd = new OperationDependency();
BeanUtils.copyProperties(d, nd);
Integer np = idMap.get(d.getPrevOperationId());
if (np != null) nd.setPrevOperationId(np);
Integer nn = idMap.get(d.getNextOperationId());
if (nn != null) nd.setNextOperationId(nn);
out.add(nd);
}
return out;
}
/** 设备选项:加工时长按批量比例缩放(准备/换型时间不随批量变化) */
private static List<MachineOption> scaleMachineOptions(List<MachineOption> mos, double scale) {
if (mos == null || mos.isEmpty()) {
return new ArrayList<>();
}
List<MachineOption> out = new ArrayList<>();
for (MachineOption mo : mos) {
MachineOption nm = new MachineOption();
BeanUtils.copyProperties(mo, nm);
nm.setProcessingTime(mo.getProcessingTime() * scale);
out.add(nm);
}
return out;
}
/** BOM 物料需求:需求数量按批量比例缩放(childOrderId 暂不拆分) */
private static List<OrderMaterialRequirement> scaleMaterialRequirements(
List<OrderMaterialRequirement> reqs, double scale) {
if (reqs == null || reqs.isEmpty()) {
return new ArrayList<>();
}
List<OrderMaterialRequirement> out = new ArrayList<>();
for (OrderMaterialRequirement omr : reqs) {
OrderMaterialRequirement nr = new OrderMaterialRequirement();
BeanUtils.copyProperties(omr, nr);
nr.setRequiredQuantity(omr.getRequiredQuantity() * scale);
out.add(nr);
}
return out;
}
private static double nz(Double v) {
return v == null ? 0 : v;
}
}
......@@ -7,6 +7,7 @@ import com.aps.entity.basic.Holiday;
import com.aps.entity.basic.Machine;
import com.aps.entity.basic.MachineOption;
import com.aps.entity.basic.MaintenanceWindow;
import com.aps.entity.basic.MaterialSupply;
import com.aps.entity.basic.SegmentType;
import com.aps.entity.basic.TimeSegment;
......@@ -341,4 +342,23 @@ public final class DataAdapter {
}
return boms;
}
/**
* 在途物料 MaterialSupply → 分时段到货 ScheduledReceipt。
* ArrivalTime 相对 timeZero 换算为分钟偏移;timeZero 或 ArrivalTime 为空时视为立即到货(t=0)。
*/
public static List<ScheduledReceipt> toScheduledReceipts(List<MaterialSupply> inTransit,
LocalDateTime timeZero) {
List<ScheduledReceipt> receipts = new ArrayList<>();
if (inTransit == null) return receipts;
for (MaterialSupply ms : inTransit) {
if (ms == null || ms.getQuantity() <= 0) continue;
double minute = 0.0;
if (ms.getArrivalTime() != null && timeZero != null) {
minute = Math.max(0, ChronoUnit.MINUTES.between(timeZero, ms.getArrivalTime()));
}
receipts.add(new ScheduledReceipt(minute, ms.getQuantity()));
}
return receipts;
}
}
package com.aps.poa.data;
import java.util.Collections;
import java.util.List;
/**
* 库存策略:时变库存模型的物料参数。
*/
......@@ -9,13 +12,15 @@ public class InventoryPolicy {
public final double safetyStock;
public final double minStock;
public final double maxStock;
public final double scheduledReceipts;
/** 在途到货(分时段):每笔带各自到货时间(相对 timeZero 的分钟偏移) */
public final List<ScheduledReceipt> scheduledReceipts;
public final double lotSize;
public final int maxPurchaseLotsPerBucket;
public final long holdingCostPerUnitPerBucket;
public InventoryPolicy(String materialId, double onHand, double safetyStock,
double minStock, double maxStock, double scheduledReceipts,
double minStock, double maxStock,
List<ScheduledReceipt> scheduledReceipts,
double lotSize, int maxPurchaseLotsPerBucket,
long holdingCostPerUnitPerBucket) {
if (minStock > safetyStock || safetyStock > maxStock) {
......@@ -27,12 +32,24 @@ public class InventoryPolicy {
this.safetyStock = safetyStock;
this.minStock = minStock;
this.maxStock = maxStock;
this.scheduledReceipts = scheduledReceipts;
this.scheduledReceipts = scheduledReceipts != null ? scheduledReceipts : Collections.emptyList();
this.lotSize = lotSize;
this.maxPurchaseLotsPerBucket = maxPurchaseLotsPerBucket;
this.holdingCostPerUnitPerBucket = holdingCostPerUnitPerBucket;
}
/** 兼容旧接口:单一在途总量 → 视为 t=0 到货的一笔 */
public InventoryPolicy(String materialId, double onHand, double safetyStock,
double minStock, double maxStock, double scheduledReceipts,
double lotSize, int maxPurchaseLotsPerBucket,
long holdingCostPerUnitPerBucket) {
this(materialId, onHand, safetyStock, minStock, maxStock,
scheduledReceipts > 0
? Collections.singletonList(new ScheduledReceipt(0, scheduledReceipts))
: Collections.<ScheduledReceipt>emptyList(),
lotSize, maxPurchaseLotsPerBucket, holdingCostPerUnitPerBucket);
}
public InventoryPolicy(String materialId, double onHand, double safetyStock,
double minStock, double maxStock, double scheduledReceipts,
double lotSize) {
......
......@@ -22,6 +22,8 @@ public class ModelConfig {
public double weightHoldingCost = 1.0;
/** 安全库存短缺惩罚权重(宏排产) */
public double weightSafetyShortfall = 1000.0;
/** 超产惩罚权重(宏排产):生产量超出需求时按超出量惩罚,驱动按需生产;与安全库存短缺同量级(超产/短缺 1:1 权衡) */
public double weightOverProduction = 1000.0;
public double maxHorizon = 10000.0;
/** 当前排产时刻(相对 timeZero 的分钟偏移):所有工序 start ≥ now 硬下界,防止排到过去 */
public double now = 0.0;
......
package com.aps.poa.data;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* MRP 展开器:从成品订单出发,沿 BOM 递归展开,自动生成半成品订单(倒排)与原材料/采购件采购需求。
*
* <p>参照 FjspCpSatDemo 的 MrpEngine 写法,分时段净需求:</p>
* <pre>
* Available(t) = OnHand(0) + Σ ScheduledReceipt(arrivalMinute ≤ t)
* Net = max(0, Gross + SafetyStock − Available(requiredDate))
* Planned = ceilToLot(Net, lotSize)
* </pre>
* <ul>
* <li>半成品(SEMI_FINISHED)Net &gt; 0:自动生成一个 BACKWARD 子订单,绑定到父订单的消耗工序;</li>
* <li>原材料/采购件(RAW/PURCHASED)Net &gt; 0:生成采购需求 {@link PurchaseRequirement}。</li>
* </ul>
*/
public final class MrpExploder {
private final Map<String, MaterialDef> materials;
private final Map<String, InventoryPolicy> policies;
private final Map<String, List<BomItemDef>> bomByParent = new HashMap<>();
private final Map<String, List<OpDef>> routingTemplates;
/** 每个物料的可用供给时间线:到货时间 → 数量(按时间升序),含现有库存 + 在途 + 已计划补货 */
private final Map<String, TreeMap<Double, Double>> supplyByMaterial = new HashMap<>();
private final MrpResult result = new MrpResult();
private int nextJobId = 1;
private MrpExploder(Map<String, MaterialDef> materials,
Map<String, InventoryPolicy> policies,
List<BomItemDef> bomItems,
Map<String, List<OpDef>> routingTemplates) {
this.materials = materials;
this.policies = policies;
this.routingTemplates = routingTemplates;
for (InventoryPolicy policy : policies.values()) {
TreeMap<Double, Double> supply = new TreeMap<>();
if (policy.onHand > 1e-9) {
supply.merge(0.0, policy.onHand, Double::sum);
}
for (ScheduledReceipt sr : policy.scheduledReceipts) {
supply.merge(sr.arrivalMinute, sr.quantity, Double::sum);
}
supplyByMaterial.put(policy.materialId, supply);
}
for (BomItemDef item : bomItems) {
bomByParent.computeIfAbsent(item.parentMaterialId, k -> new ArrayList<>()).add(item);
}
}
public static MrpResult explode(List<OrderDef> finishedOrders,
Map<String, MaterialDef> materials,
Map<String, InventoryPolicy> policies,
List<BomItemDef> bomItems,
Map<String, List<OpDef>> routingTemplates) {
MrpExploder engine = new MrpExploder(materials, policies, bomItems, routingTemplates);
List<OrderDef> sorted = new ArrayList<>(finishedOrders);
sorted.sort(Comparator.comparingDouble(o -> o.due));
for (OrderDef order : sorted) {
if (order.materialId == null || order.quantity <= 0) continue;
engine.explodeOrder(order, new HashSet<>());
}
return engine.result;
}
private void explodeOrder(OrderDef parent, Set<String> path) {
if (!path.add(parent.materialId)) {
throw new IllegalArgumentException("检测到循环 BOM: " + path + " -> " + parent.materialId);
}
for (BomItemDef item : bomByParent.getOrDefault(parent.materialId, new ArrayList<>())) {
MaterialDef child = materials.get(item.componentMaterialId);
if (child == null) {
throw new IllegalArgumentException("BOM 引用的物料不存在: " + item.componentMaterialId);
}
InventoryPolicy policy = policies.get(child.id);
if (policy == null) {
throw new IllegalArgumentException("缺少库存策略: " + child.id);
}
double gross = parent.quantity * item.qtyPer;
double requiredDate = Math.max(0, parent.due - item.supplyLeadTime);
double available = availableBefore(child.id, requiredDate);
double targetStock = Math.max(policy.safetyStock, policy.minStock);
double net = Math.max(0.0, gross + targetStock - available);
double planned = ceilToLot(net, policy.lotSize);
// 消耗按到货时间先后进行,补货在 requiredDate 到货供后续更晚需求使用
consume(child.id, requiredDate, gross);
if (planned > 0) {
addSupply(child.id, requiredDate, planned);
}
if (planned > 0 && child.type == MaterialDef.MaterialType.SEMI_FINISHED) {
OrderDef childOrder = createManufacturingOrder(parent, item, child, planned, requiredDate);
result.generatedOrders.add(childOrder);
explodeOrder(childOrder, new HashSet<>(path));
} else if (planned > 0
&& (child.type == MaterialDef.MaterialType.RAW
|| child.type == MaterialDef.MaterialType.PURCHASED)) {
result.purchases.add(new PurchaseRequirement(child.id, planned, requiredDate));
}
}
path.remove(parent.materialId);
}
private OrderDef createManufacturingOrder(OrderDef parent, BomItemDef bomItem,
MaterialDef material, double quantity, double due) {
List<OpDef> templates = routingTemplates.get(material.id);
if (templates == null || templates.isEmpty()) {
throw new IllegalArgumentException("自制物料缺少工艺模板: " + material.id);
}
String childOrderId = "MRP_" + (nextJobId++);
// 1. 建立模板工序 id → 新工序 id 映射
Map<String, String> idMap = new HashMap<>();
for (OpDef t : templates) {
idMap.put(t.id, childOrderId + "_" + t.id);
}
result.operationIdMaps.put(childOrderId, new LinkedHashMap<>(idMap));
// 2. 复制工艺模板,生成新工序(predecessors 经 idMap 重映射)
List<OpDef> childOps = new ArrayList<>();
for (OpDef t : templates) {
OpDef.Builder b = OpDef.builder(idMap.get(t.id), t.name);
for (int i = 0; i < t.candidateResources.size(); i++) {
b.addResource(t.candidateResources.get(i), t.durations.get(i));
}
b.orderId(childOrderId)
.interruptible(t.interruptible)
.setupTime(t.setupTime)
.maxWaitAfter(t.maxWaitAfter)
.unplannedCost(t.unplannedCost)
.minProductionQty(t.minProductionQty)
.maxProductionQty(t.maxProductionQty)
.lotMultiplier(t.lotMultiplier);
if (t.bufferId != null) {
b.buffer(t.bufferId, t.bufferCapacity);
}
for (String prevId : t.predecessors.keySet()) {
String newPrev = idMap.get(prevId);
if (newPrev != null) b.predecessor(newPrev);
}
childOps.add(b.build());
}
result.generatedOps.addAll(childOps);
String firstOpId = childOps.get(0).id;
String lastOpId = childOps.get(childOps.size() - 1).id;
return new OrderDef(childOrderId, firstOpId, lastOpId, -1, due,
material.id, quantity,
OrderDef.PlanDirection.BACKWARD,
parent.orderId, bomItem.consumeAtOperationId, bomItem.supplyLeadTime);
}
/** 需求时间点前(含)已到货的可用量 */
private double availableBefore(String materialId, double time) {
TreeMap<Double, Double> supply = supplyByMaterial.get(materialId);
if (supply == null) return 0.0;
double sum = 0.0;
for (double q : supply.headMap(time, true).values()) {
sum += q;
}
return sum;
}
/** 按到货时间先后消耗供给(只消耗 time ≤ requiredDate 的部分) */
private void consume(String materialId, double requiredDate, double qty) {
if (qty <= 1e-9) return;
TreeMap<Double, Double> supply = supplyByMaterial.get(materialId);
if (supply == null) return;
double remaining = qty;
Iterator<Map.Entry<Double, Double>> it = supply.headMap(requiredDate, true).entrySet().iterator();
while (it.hasNext() && remaining > 1e-9) {
Map.Entry<Double, Double> e = it.next();
double take = Math.min(remaining, e.getValue());
remaining -= take;
double left = e.getValue() - take;
if (left <= 1e-9) {
it.remove();
} else {
e.setValue(left);
}
}
}
private void addSupply(String materialId, double time, double qty) {
if (qty <= 1e-9) return;
supplyByMaterial.computeIfAbsent(materialId, k -> new TreeMap<>())
.merge(time, qty, Double::sum);
}
private static double ceilToLot(double value, double lotSize) {
if (value <= 1e-9) return 0.0;
return Math.ceil((value - 1e-9) / lotSize) * lotSize;
}
}
package com.aps.poa.data;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* MRP 展开结果:自动生成的半成品订单/工序,以及原材料/采购件的采购需求。
*/
public class MrpResult {
/** 生成的半成品工序(含成品工序之外的工序,orderId 已指向半成品订单) */
public final List<OpDef> generatedOps = new ArrayList<>();
/** 生成的半成品订单(planDirection=BACKWARD,含父订单/父工序/提前期) */
public final List<OrderDef> generatedOrders = new ArrayList<>();
/** 原材料/采购件的净采购需求 */
public final List<PurchaseRequirement> purchases = new ArrayList<>();
/** 每个生成订单的工序映射:orderId -> (模板工序 id -> 实例工序 id) */
public final Map<String, Map<String, String>> operationIdMaps = new LinkedHashMap<>();
@Override
public String toString() {
return String.format("MrpResult{半成品订单=%d, 半成品工序=%d, 采购需求=%d}",
generatedOrders.size(), generatedOps.size(), purchases.size());
}
}
......@@ -41,6 +41,12 @@ public class OpDef {
public final String bufferId;
/** WIP 缓冲区容量(同时最多等待的工件数),bufferId 非空时有效 */
public final int bufferCapacity;
/** 最小生产量(工艺级,0 表示不额外约束,实际下界为 max(需求, 此值)) */
public final double minProductionQty;
/** 最大生产量(工艺级,0 表示不额外约束) */
public final double maxProductionQty;
/** 批量倍数(工单数量必须是该数值的整数倍,1 表示无批量约束) */
public final double lotMultiplier;
private OpDef(Builder b) {
this.id = b.id;
......@@ -60,6 +66,9 @@ public class OpDef {
this.maxWaitAfter = b.maxWaitAfter;
this.bufferId = b.bufferId;
this.bufferCapacity = b.bufferCapacity;
this.minProductionQty=b.minProductionQty;
this.maxProductionQty=b.maxProductionQty;
this.lotMultiplier=b.lotMultiplier;
}
public static Builder builder(String id, String name) {
......@@ -89,6 +98,12 @@ public class OpDef {
String bufferId = null;
int bufferCapacity = 0;
double minProductionQty;
/** 最大生产量(工艺级,0 表示不额外约束) */
double maxProductionQty;
/** 批量倍数(工单数量必须是该数值的整数倍,1 表示无批量约束) */
double lotMultiplier = 1;
Builder(String id, String name) {
this.id = id;
this.name = name;
......@@ -122,6 +137,9 @@ public class OpDef {
public Builder buffer(String bufferId, int capacity) {
this.bufferId = bufferId; this.bufferCapacity = capacity; return this;
}
public Builder minProductionQty(double v) { this.minProductionQty = v; return this; }
public Builder maxProductionQty(double v) { this.maxProductionQty = v; return this; }
public Builder lotMultiplier(double v) { this.lotMultiplier = v; return this; }
public OpDef build() {
if (candidateResources.isEmpty()) {
......
......@@ -7,7 +7,7 @@ package com.aps.poa.data;
* 分别指向该订单的首/末工序。</p>
*
* <p>宏排产字段(可选):{@link #materialId} 表示该订单生产的物料,
* {@link #maxProductionQty}/{@link #productionLotSize} 让生产数量成为 CP-SAT 决策变量;
* {@link #quantity} 为需求量;生产批量/最小最大生产量定义在工艺工序(OpDef)上;
* {@link #planDirection} 支持成品正排 + 半成品倒排。</p>
*/
public class OrderDef {
......@@ -27,12 +27,6 @@ public class OrderDef {
public final String materialId;
/** 需求量 */
public final double quantity;
/** 最小生产量(≥ quantity) */
public final double minProductionQty;
/** 最大生产量(受最大库存限制) */
public final double maxProductionQty;
/** 生产批量 */
public final double productionLotSize;
/** 排程方向(成品正排 / 半成品倒排) */
public final PlanDirection planDirection;
/** 父订单 ID(半成品归属的成品订单) */
......@@ -45,14 +39,13 @@ public class OrderDef {
public OrderDef(String orderId, String firstOpId, String lastOpId,
double release, double due) {
this(orderId, firstOpId, lastOpId, release, due,
null, 0, 0, 0, 0, PlanDirection.FORWARD,
null, 0, PlanDirection.FORWARD,
null, null, 0);
}
public OrderDef(String orderId, String firstOpId, String lastOpId,
double release, double due,
String materialId, double quantity,
double minProductionQty, double maxProductionQty, double productionLotSize,
PlanDirection planDirection,
String parentOrderId, String parentOperationId, double supplyLeadTime) {
this.orderId = orderId;
......@@ -62,9 +55,6 @@ public class OrderDef {
this.due = due;
this.materialId = materialId;
this.quantity = quantity;
this.minProductionQty = minProductionQty;
this.maxProductionQty = maxProductionQty;
this.productionLotSize = productionLotSize;
this.planDirection = planDirection;
this.parentOrderId = parentOrderId;
this.parentOperationId = parentOperationId;
......
package com.aps.poa.data;
/**
* 采购需求:MRP 展开后对原材料/采购件的净采购建议。
*/
public class PurchaseRequirement {
public final String materialId;
/** 净采购量(已按 LotSize 取整) */
public final double quantity;
/** 需求日期(模型分钟):须在该日期前到货 */
public final double requiredDate;
public PurchaseRequirement(String materialId, double quantity, double requiredDate) {
this.materialId = materialId;
this.quantity = quantity;
this.requiredDate = requiredDate;
}
@Override
public String toString() {
return String.format("Purchase{material=%s, qty=%.2f, due=%.1f}", materialId, quantity, requiredDate);
}
}
package com.aps.poa.data;
/**
* 一笔在途到货:在指定到货时间到货的固定数量。
*/
public class ScheduledReceipt {
/** 到货时间:相对 timeZero 的分钟偏移(与工序 start/end 同一时间轴) */
public final double arrivalMinute;
/** 到货数量 */
public final double quantity;
public ScheduledReceipt(double arrivalMinute, double quantity) {
this.arrivalMinute = arrivalMinute;
this.quantity = quantity;
}
@Override
public String toString() {
return String.format("ScheduledReceipt{at=%.1f, qty=%.2f}", arrivalMinute, quantity);
}
}
......@@ -3,6 +3,7 @@ package com.aps.poa.model;
import com.aps.entity.basic.Entry;
import com.aps.entity.basic.Machine;
import com.aps.poa.constraint.ConstraintFactory;
import com.aps.poa.data.BatchSplitter;
import com.aps.poa.data.BomItemDef;
import com.aps.poa.data.BomRelation;
import com.aps.poa.data.DataAdapter;
......@@ -104,6 +105,10 @@ public class POAOrToolsModel {
private final Map<String, IntVar> productionLotCountVars = new LinkedHashMap<>();
/** 每个物料的时变库存 Inventory[t](宏排产) */
private final Map<String, IntVar[]> inventoryVars = new LinkedHashMap<>();
/** 每个物料的采购入库 PurchaseReceipt[t](宏排产,供结果输出) */
private final Map<String, IntVar[]> purchaseReceiptVars = new LinkedHashMap<>();
/** 每个物料的采购批次数 PurchaseLots[t](宏排产,供结果输出) */
private final Map<String, IntVar[]> purchaseLotCountVars = new LinkedHashMap<>();
/** 库存持有成本总和(进目标) */
private IntVar totalInventoryHoldingCost;
/** 安全库存短缺总和(进目标) */
......@@ -190,6 +195,16 @@ public class POAOrToolsModel {
List<SetupMatrix> setupMatrices,
List<InventoryConstraint> invConstraints,
ModelConfig config) {
this(BatchSplitter.split(entries), machines, setupMatrices, invConstraints, config, true);
}
/** entries 已完成标准批量拆分,三个适配器复用同一份拆分结果 */
private POAOrToolsModel(List<Entry> entries,
List<Machine> machines,
List<SetupMatrix> setupMatrices,
List<InventoryConstraint> invConstraints,
ModelConfig config,
boolean alreadySplit) {
this(DataAdapter.toOpDefs(entries),
DataAdapter.toResourceDefs(machines, config != null ? config.timeZero : null),
DataAdapter.toBomRelations(entries),
......@@ -300,6 +315,8 @@ public class POAOrToolsModel {
public Map<String, IntVar> getProductionQtyVars() { return productionQtyVars; }
public Map<String, IntVar> getProductionLotCountVars() { return productionLotCountVars; }
public Map<String, IntVar[]> getInventoryVars() { return inventoryVars; }
public Map<String, IntVar[]> getPurchaseReceiptVars() { return purchaseReceiptVars; }
public Map<String, IntVar[]> getPurchaseLotCountVars() { return purchaseLotCountVars; }
public IntVar getTotalInventoryHoldingCost() { return totalInventoryHoldingCost; }
public void setTotalInventoryHoldingCost(IntVar v) { this.totalInventoryHoldingCost = v; }
public IntVar getTotalSafetyStockShortfall() { return totalSafetyStockShortfall; }
......
......@@ -146,15 +146,29 @@ public final class ObjectiveBuilder {
}
}
/** 库存持有成本 + 安全库存短缺(宏排产,进目标) */
/** 库存持有成本 + 安全库存短缺 + 超产惩罚(宏排产,进目标) */
private static void addInventoryCostTerms(POAOrToolsModel model) {
// 数量类目标变量(库存/短缺/产量)按 QUANTITY_SCALE 缩放,系数需除以 QUANTITY_SCALE 抵消,
// 否则持有成本/短缺惩罚相对未规划惩罚被放大 1000 倍,导致权重失衡(如成品被误判为未规划更划算)
long holdingCoeff = Math.max(1L,
model.lscale(model.getConfig().weightHoldingCost) / POAOrToolsModel.QUANTITY_SCALE);
long shortfallCoeff = Math.max(1L,
model.lscale(model.getConfig().weightSafetyShortfall) / POAOrToolsModel.QUANTITY_SCALE);
long overProdCoeff = Math.max(1L,
model.lscale(model.getConfig().weightOverProduction) / POAOrToolsModel.QUANTITY_SCALE);
if (model.getTotalInventoryHoldingCost() != null) {
model.getObjectiveVars().add(model.getTotalInventoryHoldingCost());
model.getObjectiveCoeffs().add(model.lscale(model.getConfig().weightHoldingCost));
model.getObjectiveCoeffs().add(holdingCoeff);
}
if (model.getTotalSafetyStockShortfall() != null) {
model.getObjectiveVars().add(model.getTotalSafetyStockShortfall());
model.getObjectiveCoeffs().add(model.lscale(model.getConfig().weightSafetyShortfall));
model.getObjectiveCoeffs().add(shortfallCoeff);
}
// 超产惩罚:驱动生产量贴近需求。否则成品不计持有成本时,生产量失去下压动机会任意飘高
for (IntVar qty : model.getProductionQtyVars().values()) {
model.getObjectiveVars().add(qty);
model.getObjectiveCoeffs().add(overProdCoeff);
}
}
}
......@@ -15,6 +15,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
......@@ -62,6 +63,9 @@ public class RoutingDataService {
@Autowired
private EquipinfoService _equipinfoService;
@Autowired
private RoutingDetailService _routingDetailService;
public Map<Integer, Object> InitEntrys(String SceneId, List<ProdEquipment> ProdEquipments, List<Order> ProdLaunchOrders)
{
return InitEntrys(SceneId, ProdEquipments, ProdLaunchOrders, null);
......@@ -92,7 +96,7 @@ public class RoutingDataService {
.eq(ProdOrderProcess::getSceneId,SceneId)
.list();
return CreateEntry( SceneId, ProdEquipments, ProdLaunchOrders, routingDiscreteParams, ProdOrderProcesss, ProdProcessExecs,null,0 );
return CreateEntry( SceneId, ProdEquipments, ProdLaunchOrders, routingDiscreteParams, ProdOrderProcesss, ProdProcessExecs,null,0 );
}
......@@ -115,6 +119,28 @@ public class RoutingDataService {
return routingDiscreteParams;
}
private Map<Long, RoutingDetail> getRoutingDetailsByRoutingDetailIds(List<Long> routingDetailIds) {
Map<Long, RoutingDetail> map = new HashMap<>();
if (routingDetailIds == null || routingDetailIds.isEmpty()) {
return map;
}
for (int i = 0; i < routingDetailIds.size(); i += ORACLE_IN_BATCH_SIZE) {
int endIndex = Math.min(i + ORACLE_IN_BATCH_SIZE, routingDetailIds.size());
List<Long> batchIds = routingDetailIds.subList(i, endIndex);
List<RoutingDetail> batchDetails = _routingDetailService.lambdaQuery()
.in(RoutingDetail::getId, batchIds)
.list();
for (RoutingDetail rd : batchDetails) {
map.put(rd.getId(), rd);
}
}
return map;
}
private static Double toDouble(BigDecimal v) {
return v == null ? null : v.doubleValue();
}
public Map<Integer, Object> CreateEntry(String SceneId, List<ProdEquipment> ProdEquipments, List<Order> ProdLaunchOrders, List<RoutingDiscreteParam> routingDiscreteParams, List<ProdOrderProcess> ProdOrderProcesss, List<ProdProcessExec> ProdProcessExecs, List<GroupResult> existingResults,int FinishOpertionID)
{
return CreateEntry(SceneId, ProdEquipments, ProdLaunchOrders, routingDiscreteParams, ProdOrderProcesss, ProdProcessExecs, existingResults, FinishOpertionID, null);
......@@ -125,12 +151,12 @@ public class RoutingDataService {
Map<Integer, Object> list=new HashMap<>();
List<String> soutceExecId = ProdOrderProcesss.stream()
.map(ProdOrderProcess::getExecId)
// .distinct() // 提取Exec_ID
// .distinct() // 提取Exec_ID
.collect(Collectors.toList());
List<String> targetExecId = ProdOrderProcesss.stream()
.map(ProdOrderProcess::getTargetExecId)
// .distinct() // 提取TARGET_Exec_ID
// .distinct() // 提取TARGET_Exec_ID
.collect(Collectors.toList());
List<String> ExecIdNoChild= ProdProcessExecs.stream()
......@@ -155,6 +181,14 @@ public class RoutingDataService {
results = IdGroupingWithDualSerial.addNewDataWithIsolatedGroup(existingResults,soutceExecId, targetExecId);
}
// 查询工艺路线工序的批量参数(batchQty / splitMinQty / splitMaxQty / maxProductionQty)
List<Long> routingDetailIds = ProdProcessExecs.stream()
.map(ProdProcessExec::getRoutingDetailId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
Map<Long, RoutingDetail> routingDetailMap = getRoutingDetailsByRoutingDetailIds(routingDetailIds);
List<Entry> entrys=new ArrayList<>();
Map<Long,Double> machineIds=new HashMap<>();
for (int i = index; i < results.size(); i++) {
......@@ -244,6 +278,13 @@ public class RoutingDataService {
entry.setRoutingCode(op.getRoutingCode());
entry.setRoutingName(op.getRoutingName());
entry.setRoutingDetailId(op.getRoutingDetailId());
RoutingDetail rd = op.getRoutingDetailId() != null ? routingDetailMap.get(op.getRoutingDetailId()) : null;
if (rd != null) {
entry.setBatchQty(toDouble(rd.getBatchQty()));
entry.setSplitMinQty(toDouble(rd.getSplitMinQty()));
entry.setSplitMaxQty(toDouble(rd.getSplitMaxQty()));
entry.setMaxProductionQty(toDouble(rd.getMaxProductionQty()));
}
entry.setTaskSeq(op.getTaskSeq());
entry.setRoutingDetailName(op.getRoutingDetailName());
if (op.getDepartmentId() != null) {
......@@ -259,6 +300,7 @@ public class RoutingDataService {
entry.setProductCode(order.getMaterialCode());
entry.setProductName(order.getMaterialName());
entry.setPriority(order.getActualPriority());
entry.setCanSplit(order.isCanSplit());
order.setId(entry.getGroupId());
}
......@@ -281,10 +323,10 @@ public class RoutingDataService {
minProcessingTime=Math.min(minProcessingTime,totalprocessTime);
if(machineIds.containsKey(e.getEquipId()))
{
if( machineIds.get(e.getEquipId())<totalprocessTime)
{
machineIds.replace(e.getEquipId(),totalprocessTime);
}
if( machineIds.get(e.getEquipId())<totalprocessTime)
{
machineIds.replace(e.getEquipId(),totalprocessTime);
}
}else {
//大概记录要用到的设备的加工时间,用于生成设备日历
machineIds.put(e.getEquipId(),totalprocessTime);
......@@ -300,22 +342,22 @@ public class RoutingDataService {
mo.setEquipName(e.getEquipName());
mo.setResourceCode(e.getResourceCode());
mo.setProcessingTime(e.getSpeed());
// mo.setContantTime(op.getConstTime());
// mo.setSetupTime(op.getChangeLineTime());
// mo.setTeardownTime(op.getPostprocessingTime());
// mo.setPreTime(e.getSetupTime());
// mo.setContantTime(op.getConstTime());
// mo.setSetupTime(op.getChangeLineTime());
// mo.setTeardownTime(op.getPostprocessingTime());
// mo.setPreTime(e.getSetupTime());
mos.add(mo);
}
entry.setMinProcessingTime(minProcessingTime);
entry.setMinProcessingTime(minProcessingTime);
entry.setMachineOptions(mos);
}
}
if(entry.getMachineOptions()!=null)
{
entrys.add(entry);
}
if(entry.getMachineOptions()!=null)
{
entrys.add(entry);
}
}
......@@ -566,7 +608,7 @@ if(entry.getMachineOptions()!=null)
//特殊日历
LambdaQueryWrapper<SpecialCalendarDetail> SpecialCalendarDetailWrapper = new LambdaQueryWrapper<>();
SpecialCalendarDetailWrapper.eq(SpecialCalendarDetail::getIsDeleted, 0);
// SpecialCalendarDetailWrapper.ge(SpecialCalendarDetail::getEndTime, baseTime);
// SpecialCalendarDetailWrapper.ge(SpecialCalendarDetail::getEndTime, baseTime);
List<SpecialCalendarDetail> SpecialCalendarDetails = _specialCalendarDetailService.list(SpecialCalendarDetailWrapper);
List<Equipinfo> equipinfoList = _equipinfoService.lambdaQuery()
......@@ -577,8 +619,8 @@ if(entry.getMachineOptions()!=null)
Machine machine = new Machine();
machine.setId(resource.getId());
Equipinfo equipinfo= equipinfoList.stream()
.filter(t->t.getId().equals(resource.getReferenceId()))
.findFirst().orElse(null);
.filter(t->t.getId().equals(resource.getReferenceId()))
.findFirst().orElse(null);
machine.setDepartment(resource.getDepartTitle());
if(equipinfo!=null)
{
......@@ -586,125 +628,125 @@ if(entry.getMachineOptions()!=null)
machine.setName(equipinfo.getEquipName());
machine.setCapacityTypeName(equipinfo.getCapacityTypeName());
}else {
machine.setCode(resource.getReferenceCode());
machine.setName(resource.getTitle());
machine.setCode(resource.getReferenceCode());
machine.setName(resource.getTitle());
}
List<EquipCapacityDef> machineProdEquipSpecialCals = ProdEquipSpecialCals.stream()
.filter(t -> t.getPlanResourceId() != null &&t.getReferenceId() != null && t.getPlanResourceId() == machine.getId() && t.getReferenceType() == 1)
.collect(Collectors.toList());
List<Shift> shifts1 = new ArrayList<>();
for (EquipCapacityDef machineProdEquipSpecialCal : machineProdEquipSpecialCals) {
List<MesShiftWorkSched> ShiftWorkScheds = MesShiftWorkScheds.stream()
.filter(t -> (long) t.getWeekWorkSchedId() == machineProdEquipSpecialCal.getReferenceId())
.collect(Collectors.toList());
List<Shift> Shifts = mergeShiftData(ShiftWorkScheds);
for (Shift shift : Shifts) {
List<EquipCapacityDef> machineProdEquipSpecialCals = ProdEquipSpecialCals.stream()
.filter(t -> t.getPlanResourceId() != null &&t.getReferenceId() != null && t.getPlanResourceId() == machine.getId() && t.getReferenceType() == 1)
.collect(Collectors.toList());
List<Shift> shifts1 = new ArrayList<>();
for (EquipCapacityDef machineProdEquipSpecialCal : machineProdEquipSpecialCals) {
shift.setMachineId(machine.getId());
shift.setStartDate(machineProdEquipSpecialCal.getEffectiveStartTime());
shift.setEndDate(machineProdEquipSpecialCal.getEffectiveEndTime());
if(machineProdEquipSpecialCal.getEfficiencyCoeff()!=null) {
shift.setEfficiency(machineProdEquipSpecialCal.getEfficiencyCoeff());
}
shifts1.add(shift);
List<MesShiftWorkSched> ShiftWorkScheds = MesShiftWorkScheds.stream()
.filter(t -> (long) t.getWeekWorkSchedId() == machineProdEquipSpecialCal.getReferenceId())
.collect(Collectors.toList());
List<Shift> Shifts = mergeShiftData(ShiftWorkScheds);
for (Shift shift : Shifts) {
shift.setMachineId(machine.getId());
shift.setStartDate(machineProdEquipSpecialCal.getEffectiveStartTime());
shift.setEndDate(machineProdEquipSpecialCal.getEffectiveEndTime());
if(machineProdEquipSpecialCal.getEfficiencyCoeff()!=null) {
shift.setEfficiency(machineProdEquipSpecialCal.getEfficiencyCoeff());
}
}
shifts1.add(shift);
}
}
if(resource.getWorkSchedId()!=null) {
List<MesShiftWorkSched> ShiftWorkScheds = MesShiftWorkScheds.stream()
.filter(t -> (long) t.getWeekWorkSchedId() == resource.getWorkSchedId())
.collect(Collectors.toList());
List<Shift> Shifts = mergeShiftData(ShiftWorkScheds);
for (Shift shift : Shifts) {
shift.setMachineId(machine.getId());
if(resource.getWorkSchedId()!=null) {
List<MesShiftWorkSched> ShiftWorkScheds = MesShiftWorkScheds.stream()
.filter(t -> (long) t.getWeekWorkSchedId() == resource.getWorkSchedId())
.collect(Collectors.toList());
List<Shift> Shifts = mergeShiftData(ShiftWorkScheds);
for (Shift shift : Shifts) {
shift.setStartDate(LocalDateTime.of(2000, 1, 1, 0, 0, 0));
shift.setEndDate(LocalDateTime.of(2000, 1, 1, 0, 0, 0));
shifts1.add(shift);
}
}else {
Shift shift=new Shift();
shift.setMachineId(machine.getId());
shift.setStartTime(LocalTime.of(0,0,0));
shift.setEndTime(LocalTime.of(23,59,59));
HashSet days= new HashSet<>();
days.add(1);
days.add(2);
days.add(3);
days.add(4);
days.add(5);
days.add(6);
days.add(0);
shift.setDays(days);
shift.setStartDate(LocalDateTime.of(2000, 1, 1, 0, 0, 0));
shift.setEndDate(LocalDateTime.of(2000, 1, 1, 0, 0, 0));
shifts1.add(shift);
}
machine.setShifts(shifts1);
}else {
Shift shift=new Shift();
shift.setMachineId(machine.getId());
shift.setStartTime(LocalTime.of(0,0,0));
shift.setEndTime(LocalTime.of(23,59,59));
HashSet days= new HashSet<>();
days.add(1);
days.add(2);
days.add(3);
days.add(4);
days.add(5);
days.add(6);
days.add(0);
shift.setDays(days);
shift.setStartDate(LocalDateTime.of(2000, 1, 1, 0, 0, 0));
shift.setEndDate(LocalDateTime.of(2000, 1, 1, 0, 0, 0));
shifts1.add(shift);
}
machine.setShifts(shifts1);
//加班
List<EquipCapacityDef> machineProdEquipSpecialCals2 = ProdEquipSpecialCals.stream()
.filter(t -> t.getPlanResourceId() != null && t.getPlanResourceId() == machine.getId() && t.getReferenceType() == 3)
//加班
List<EquipCapacityDef> machineProdEquipSpecialCals2 = ProdEquipSpecialCals.stream()
.filter(t -> t.getPlanResourceId() != null && t.getPlanResourceId() == machine.getId() && t.getReferenceType() == 3)
.collect(Collectors.toList());
List<DateRange> shifts2 = new ArrayList<>();
for (EquipCapacityDef machineProdEquipSpecialCal : machineProdEquipSpecialCals2) {
List<SpecialCalendarDetail> SpecialCalendarDetails1 = SpecialCalendarDetails.stream()
.filter(t -> (long) t.getSpecialCalendarId() == machineProdEquipSpecialCal.getReferenceId())
.collect(Collectors.toList());
List<DateRange> shifts2 = new ArrayList<>();
for (EquipCapacityDef machineProdEquipSpecialCal : machineProdEquipSpecialCals2) {
List<SpecialCalendarDetail> SpecialCalendarDetails1 = SpecialCalendarDetails.stream()
.filter(t -> (long) t.getSpecialCalendarId() == machineProdEquipSpecialCal.getReferenceId())
.collect(Collectors.toList());
for (SpecialCalendarDetail cald : SpecialCalendarDetails1) {
DateRange shift = new DateRange(cald.getStartTime(),cald.getEndTime());
shifts2.add(shift);
}
for (SpecialCalendarDetail cald : SpecialCalendarDetails1) {
DateRange shift = new DateRange(cald.getStartTime(),cald.getEndTime());
shifts2.add(shift);
}
List<EquipCapacityDef> Holidays = ProdEquipSpecialCals.stream()
.filter(t -> t.getPlanResourceId() != null && t.getPlanResourceId() == machine.getId() && t.getReferenceType() == 2)
.collect(Collectors.toList());
List<DateRange> shifts3 = new ArrayList<>();
for (EquipCapacityDef machineProdEquipSpecialCal : Holidays) {
}
DateRange shift = new DateRange(machineProdEquipSpecialCal.getEffectiveStartTime(),machineProdEquipSpecialCal.getEffectiveEndTime());
List<EquipCapacityDef> Holidays = ProdEquipSpecialCals.stream()
.filter(t -> t.getPlanResourceId() != null && t.getPlanResourceId() == machine.getId() && t.getReferenceType() == 2)
.collect(Collectors.toList());
List<DateRange> shifts3 = new ArrayList<>();
for (EquipCapacityDef machineProdEquipSpecialCal : Holidays) {
shifts3.add(shift);
}
DateRange shift = new DateRange(machineProdEquipSpecialCal.getEffectiveStartTime(),machineProdEquipSpecialCal.getEffectiveEndTime());
List<DateRange> Holidaysn= RangeSubtractUtil.getNonOverlappingRanges(shifts3,shifts2);
shifts3.add(shift);
}
List<Holiday> Holidays1 = new ArrayList<>();
for (DateRange cal : Holidaysn) {
List<DateRange> Holidaysn= RangeSubtractUtil.getNonOverlappingRanges(shifts3,shifts2);
Holiday holiday = new Holiday();
holiday.setStart(cal.getStartDate());
holiday.setEnd(cal.getEndDate());
Holidays1.add(holiday);
}
machine.setHolidays(Holidays1);
List<Holiday> Holidays1 = new ArrayList<>();
for (DateRange cal : Holidaysn) {
List<MaintenanceWindow> maintenanceWindows=new ArrayList<>();
Holiday holiday = new Holiday();
holiday.setStart(cal.getStartDate());
holiday.setEnd(cal.getEndDate());
Holidays1.add(holiday);
}
machine.setHolidays(Holidays1);
List<EquipMaintainTask> EquipMaintainTasks1 = EquipMaintainTasks.stream()
.filter(t -> t.getEquipId().equals(resource.getReferenceId()) )
.collect(Collectors.toList());
for (EquipMaintainTask equipMaintainTask : EquipMaintainTasks1) {
MaintenanceWindow maintenanceWindow=new MaintenanceWindow();
List<MaintenanceWindow> maintenanceWindows=new ArrayList<>();
List<EquipMaintainTask> EquipMaintainTasks1 = EquipMaintainTasks.stream()
.filter(t -> t.getEquipId().equals(resource.getReferenceId()) )
.collect(Collectors.toList());
for (EquipMaintainTask equipMaintainTask : EquipMaintainTasks1) {
MaintenanceWindow maintenanceWindow=new MaintenanceWindow();
// maintenanceWindow.setId(equipMaintainTask.getId().toString());
maintenanceWindow.setId(UUID.randomUUID().toString());
maintenanceWindow.setStartTime(equipMaintainTask.getPlanStartTime());
maintenanceWindow.setEndTime(equipMaintainTask.getPlanFinishTime());
maintenanceWindow.setEquipCode(equipMaintainTask.getEquipCode());
maintenanceWindow.setEquipName(equipMaintainTask.getEquipName());
maintenanceWindow.setReason("");
maintenanceWindows.add(maintenanceWindow);
}
maintenanceWindow.setId(UUID.randomUUID().toString());
maintenanceWindow.setStartTime(equipMaintainTask.getPlanStartTime());
maintenanceWindow.setEndTime(equipMaintainTask.getPlanFinishTime());
maintenanceWindow.setEquipCode(equipMaintainTask.getEquipCode());
maintenanceWindow.setEquipName(equipMaintainTask.getEquipName());
maintenanceWindow.setReason("");
maintenanceWindows.add(maintenanceWindow);
}
machine.setMaintenanceWindows(maintenanceWindows);
machine.setMaintenanceWindows(maintenanceWindows);
machines.add(machine);
......@@ -758,25 +800,25 @@ if(entry.getMachineOptions()!=null)
}
List<Shift> shifts1 = new ArrayList<>();
Shift shift=new Shift();
shift.setMachineId(machine.getId());
shift.setStartTime(LocalTime.of(0,0,0));
shift.setEndTime(LocalTime.of(23,59,59));
HashSet days= new HashSet<>();
days.add(1);
days.add(2);
days.add(3);
days.add(4);
days.add(5);
days.add(6);
days.add(0);
shift.setDays(days);
shift.setStartDate(LocalDateTime.of(2000, 1, 1, 0, 0, 0));
shift.setEndDate(LocalDateTime.of(2000, 1, 1, 0, 0, 0));
shifts1.add(shift);
machine.setShifts(shifts1);
List<Shift> shifts1 = new ArrayList<>();
Shift shift=new Shift();
shift.setMachineId(machine.getId());
shift.setStartTime(LocalTime.of(0,0,0));
shift.setEndTime(LocalTime.of(23,59,59));
HashSet days= new HashSet<>();
days.add(1);
days.add(2);
days.add(3);
days.add(4);
days.add(5);
days.add(6);
days.add(0);
shift.setDays(days);
shift.setStartDate(LocalDateTime.of(2000, 1, 1, 0, 0, 0));
shift.setEndDate(LocalDateTime.of(2000, 1, 1, 0, 0, 0));
shifts1.add(shift);
machine.setShifts(shifts1);
List<MaintenanceWindow> maintenanceWindows=new ArrayList<>();
......
package com.aps.demo;
import com.aps.entity.Algorithm.OperationDependency;
import com.aps.entity.basic.Entry;
import com.aps.entity.basic.MachineOption;
import com.aps.poa.data.BatchSplitter;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* 标准批量拆分(BatchSplitter)单元测试。
*/
public class POABatchSplitTest {
// ==================== splitQuantities:纯算法 ====================
@Test
public void testNoSplit() {
// 不可拆分
assertEquals(Arrays.asList(1000.0),
BatchSplitter.splitQuantities(1000, 500.0, 100.0, 600.0, 800.0, false));
// 未超过最大生产量
assertEquals(Arrays.asList(800.0),
BatchSplitter.splitQuantities(800, 500.0, 100.0, 600.0, 800.0, true));
// 未超过标准批量
assertEquals(Arrays.asList(500.0),
BatchSplitter.splitQuantities(500, 500.0, 100.0, 600.0, 800.0, true));
}
@Test
public void testEqualSplit() {
assertEquals(Arrays.asList(500.0, 500.0),
BatchSplitter.splitQuantities(1000, 500.0, 100.0, 600.0, 800.0, true));
}
@Test
public void testTailMerge() {
// 尾批 1 < splitMinQty(100),并入前一批 → [500, 501]
assertEquals(Arrays.asList(500.0, 501.0),
BatchSplitter.splitQuantities(1001, 500.0, 100.0, 600.0, 800.0, true));
}
@Test
public void testTailKept() {
// 尾批 200 ≥ splitMinQty(100),保留 → [500, 500, 200]
assertEquals(Arrays.asList(500.0, 500.0, 200.0),
BatchSplitter.splitQuantities(1200, 500.0, 100.0, 600.0, 800.0, true));
}
@Test
public void testClampToSplitRange() {
// batchQty(150) < splitMinQty(200) → 每批 200
assertEquals(Arrays.asList(200.0, 200.0, 200.0, 200.0, 200.0),
BatchSplitter.splitQuantities(1000, 150.0, 200.0, 400.0, 800.0, true));
// batchQty(700) > splitMaxQty(400) → 每批 400,尾批 200
assertEquals(Arrays.asList(400.0, 400.0, 200.0),
BatchSplitter.splitQuantities(1000, 700.0, 100.0, 400.0, 800.0, true));
}
// ==================== split:Entry 级订单拆分 ====================
@Test
public void testEntrySplit() {
Entry e1 = op(1, "FG1", 1, "工序0");
Entry e2 = op(2, "FG1", 2, "工序1");
Entry e3 = op(3, "FG1", 3, "工序2");
Entry e4 = op(4, "FG1", 4, "工序3");
e4.setProductId("FG1");
e4.setQuantity(1000);
List<Entry> order = Arrays.asList(e1, e2, e3, e4);
for (Entry e : order) {
e.setBatchQty(500.0);
e.setSplitMinQty(100.0);
e.setSplitMaxQty(600.0);
e.setMaxProductionQty(800.0);
e.setCanSplit(true);
}
dep(e2, e1);
dep(e3, e2);
dep(e4, e3);
List<Entry> result = BatchSplitter.split(order);
// 2 批 → 8 道工序
assertEquals(8, result.size());
// 两个子订单:FG1_b1 / FG1_b2
List<String> orderIds = result.stream().map(Entry::getOrderId).distinct().sorted().collect(Collectors.toList());
assertEquals(Arrays.asList("FG1_b1", "FG1_b2"), orderIds);
// 每个子订单 4 道工序,数量 = 500
for (String oid : orderIds) {
List<Entry> ops = result.stream().filter(e -> oid.equals(e.getOrderId()))
.sorted((a, b) -> Integer.compare(a.getSequence(), b.getSequence()))
.collect(Collectors.toList());
assertEquals(4, ops.size());
for (Entry e : ops) {
assertEquals(500.0, e.getQuantity());
assertEquals(Integer.valueOf(1), e.getState());
}
}
// 子订单内前序依赖已重映射到本批内部工序 id
List<Entry> b1 = result.stream().filter(e -> "FG1_b1".equals(e.getOrderId())).collect(Collectors.toList());
Entry b1Seq2 = b1.stream().filter(e -> e.getSequence() == 2).findFirst()
.orElseThrow(() -> new IllegalStateException("seq2 not found"));
assertEquals(1, b1Seq2.getPrevEntryIds().size());
int prevId = b1Seq2.getPrevEntryIds().get(0).getPrevOperationId();
Entry b1Seq1 = b1.stream().filter(e -> e.getSequence() == 1).findFirst()
.orElseThrow(() -> new IllegalStateException("seq1 not found"));
assertEquals(b1Seq1.getId(), prevId);
}
// ==================== 辅助 ====================
private static Entry op(int id, String orderId, int sequence, String name) {
Entry e = new Entry();
e.setId(id);
e.setOrderId(orderId);
e.setSequence(sequence);
e.setRoutingDetailName(name);
e.setPriority(100);
e.setMachineOptions(Arrays.asList(opt(0L, 60)));
return e;
}
private static MachineOption opt(Long machineId, double processingTime) {
MachineOption mo = new MachineOption();
mo.setMachineId(machineId);
mo.setProcessingTime(processingTime);
return mo;
}
private static void dep(Entry entry, Entry prev) {
OperationDependency d = new OperationDependency();
d.setPrevOperationId(prev.getId());
entry.getPrevEntryIds().add(d);
}
}
package com.aps.demo;
import com.aps.poa.data.BomItemDef;
import com.aps.poa.data.BomRelation;
import com.aps.poa.data.InventoryConstraint;
import com.aps.poa.data.InventoryPolicy;
import com.aps.poa.data.MaterialDef;
import com.aps.poa.data.ModelConfig;
import com.aps.poa.data.OpDef;
import com.aps.poa.data.OrderDef;
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 com.google.ortools.sat.CpSolver;
import com.google.ortools.sat.CpSolverStatus;
import com.google.ortools.sat.IntVar;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 宏排产补充功能测试:最大等待时间 maxWait、WIP 缓冲区容量、时变库存+采购+生产数量、混合方向。
* 运行:java -cp &lt;classpath&gt; com.aps.demo.POAMacroPlanTest
*/
public class POAMacroPlanTest {
public static void main(String[] args) {
Loader.loadNativeLibraries();
testMaxWait();
// testWipCapacity();
testMaterialInventory();
System.out.println("\n[PASS] 宏排产补充功能测试全部通过");
}
/** 最大等待时间:BACKWARD 下后工序想尽量后推,但受 maxWait 限制 */
static void testMaxWait() {
List<OpDef> ops = new ArrayList<>();
ops.add(OpDef.builder("A", "工序A").addResource("M0", 10).orderId("J0")
.maxWaitAfter(5).unplannedCost(100000).build());
ops.add(OpDef.builder("B", "工序B").addResource("M1", 10).orderId("J0")
.predecessor("A").unplannedCost(100000).build());
List<ResourceDef> resources = Arrays.asList(new ResourceDef("M0", 200), new ResourceDef("M1", 200));
ModelConfig cfg = new ModelConfig();
cfg.maxHorizon = 200;
cfg.scheduleMode = ModelConfig.ScheduleMode.BACKWARD;
cfg.timeLimitSeconds = 10;
POAOrToolsModel scheduler = new POAOrToolsModel(
ops, resources, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), cfg);
scheduler.build();
SolveResult r = scheduler.solve();
double wait = r.startTimes.get("B") - r.endTimes.get("A");
if (wait > 5 + 1e-6) {
throw new AssertionError("maxWait 约束违反: B.start - A.end = " + wait + " > 5");
}
System.out.println("[PASS] maxWait: B.start - A.end = " + wait + " ≤ 5");
}
/** WIP 缓冲区容量:两个工单的半成品在 M0→M1 间等待,缓冲区容量=1 */
static void testWipCapacity() {
List<OpDef> ops = new ArrayList<>();
// J0: A0(M0) -> B0(M1),A0 完成后进入 BUF 等待 B0
ops.add(OpDef.builder("A0", "A0").addResource("M0", 20).orderId("J0")
.buffer("BUF", 1).unplannedCost(100000).build());
ops.add(OpDef.builder("B0", "B0").addResource("M1", 20).orderId("J0")
.predecessor("A0").unplannedCost(100000).build());
// J1: A1(M0) -> B1(M1)
ops.add(OpDef.builder("A1", "A1").addResource("M0", 20).orderId("J1")
.buffer("BUF", 1).unplannedCost(100000).build());
ops.add(OpDef.builder("B1", "B1").addResource("M1", 20).orderId("J1")
.predecessor("A1").unplannedCost(100000).build());
List<ResourceDef> resources = Arrays.asList(new ResourceDef("M0", 200), new ResourceDef("M1", 200));
ModelConfig cfg = new ModelConfig();
cfg.maxHorizon = 200;
cfg.timeLimitSeconds = 10;
POAOrToolsModel scheduler = new POAOrToolsModel(
ops, resources, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), cfg);
scheduler.build();
SolveResult r = scheduler.solve();
if (!r.isFeasible || !r.unscheduledOps.isEmpty()) {
throw new AssertionError("WIP 场景应可行且全规划: " + r);
}
System.out.println("[PASS] WIP 缓冲区容量: makespan=" + r.makespan + ", 全规划");
}
/** 时变库存 + 采购 + 生产数量 + 混合方向(成品正排 + 半成品倒排) */
static void testMaterialInventory() {
// 物料
List<MaterialDef> materials = Arrays.asList(
new MaterialDef("FG", "FG", "成品", MaterialDef.MaterialType.FINISHED),
new MaterialDef("SUB", "SUB", "半成品", MaterialDef.MaterialType.SEMI_FINISHED),
new MaterialDef("RAW", "RAW", "原料", MaterialDef.MaterialType.RAW));
// 库存策略:RAW 可采购,lotSize=5,最多 10 批/桶
List<InventoryPolicy> policies = Arrays.asList(
new InventoryPolicy("FG", 0, 0, 0, 100, 0, 10),
new InventoryPolicy("SUB", 0, 0, 0, 100, 0, 10),
new InventoryPolicy("RAW", 0, 0, 0, 100, 0, 5, 10, 1L));
// BOM:1 FG = 2 SUB(在 FG_O0 开工消耗);1 SUB = 1 RAW(在 SUB_O0 开工消耗)
List<BomItemDef> bom = Arrays.asList(
new BomItemDef("FG", "SUB", 2.0, "FG_O0", 0),
new BomItemDef("SUB", "RAW", 1.0, "SUB_O0", 0));
// 工序:FG 与 SUB 各一道,均在 M0
List<OpDef> ops = Arrays.asList(
OpDef.builder("FG_O0", "成品工序").addResource("M0", 10).orderId("FG_ORDER").unplannedCost(100000).build(),
OpDef.builder("SUB_O0", "半成品工序").addResource("M0", 10).orderId("SUB_ORDER").unplannedCost(100000).build());
// 订单:FG 正排生产 10(release=40,给半成品留出生产时间);SUB 倒排生产 20,父订单 FG_ORDER,在 FG_O0 开工前 0 分钟到位
List<OrderDef> orders = Arrays.asList(
new OrderDef("FG_ORDER", "FG_O0", "FG_O0", 40, 60,
"FG", 10, 10, 10, 10, OrderDef.PlanDirection.FORWARD, null, null, 0),
new OrderDef("SUB_ORDER", "SUB_O0", "SUB_O0", -1, 60,
"SUB", 20, 20, 20, 10, OrderDef.PlanDirection.BACKWARD,
"FG_ORDER", "FG_O0", 0));
List<ResourceDef> resources = Arrays.asList(new ResourceDef("M0", 200));
ModelConfig cfg = new ModelConfig();
cfg.maxHorizon = 60; // 时变库存时间桶 = 60 分钟
cfg.timeLimitSeconds = 30;
POAOrToolsModel scheduler = new POAOrToolsModel(
ops, resources, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(),
orders, cfg, materials, policies, bom);
scheduler.build();
CpSolver solver = new CpSolver();
solver.getParameters().setMaxTimeInSeconds(30);
CpSolverStatus status = solver.solve(scheduler.getRawModel());
if (status != CpSolverStatus.OPTIMAL && status != CpSolverStatus.FEASIBLE) {
throw new AssertionError("时变库存场景求解失败: " + status);
}
long fgQty = solver.value(scheduler.getProductionQtyVars().get("FG_ORDER"));
long subQty = solver.value(scheduler.getProductionQtyVars().get("SUB_ORDER"));
System.out.printf("[PASS] 时变库存: 状态=%s, FG生产=%d, SUB生产=%d%n",
status, fgQty, subQty);
// FG 需求 10,SUB 需求 20(10×2)
if (fgQty < 10 * POAOrToolsModel.QUANTITY_SCALE) {
throw new AssertionError("FG 生产量应 >= 10,实际=" + fgQty);
}
if (subQty < 20 * POAOrToolsModel.QUANTITY_SCALE) {
throw new AssertionError("SUB 生产量应 >= 20,实际=" + subQty);
}
}
}
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