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

Merge remote-tracking branch 'origin/tl'

parents c6e3d8ce bf94442c
This diff is collapsed.
...@@ -81,6 +81,28 @@ public class Entry { ...@@ -81,6 +81,28 @@ public class Entry {
* 数量 * 数量
*/ */
private double quantity; 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 { ...@@ -113,10 +135,10 @@ public class Entry {
*/ */
private Integer state ; private Integer state ;
/** /**
* 是否可中断,间缝插针 * 是否可中断,间缝插针
*/ */
private Long isInterrupt = 1l; private Long isInterrupt = 1l;
/** /**
* 所需物料 * 所需物料
......
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; ...@@ -7,6 +7,7 @@ import com.aps.entity.basic.Holiday;
import com.aps.entity.basic.Machine; import com.aps.entity.basic.Machine;
import com.aps.entity.basic.MachineOption; import com.aps.entity.basic.MachineOption;
import com.aps.entity.basic.MaintenanceWindow; import com.aps.entity.basic.MaintenanceWindow;
import com.aps.entity.basic.MaterialSupply;
import com.aps.entity.basic.SegmentType; import com.aps.entity.basic.SegmentType;
import com.aps.entity.basic.TimeSegment; import com.aps.entity.basic.TimeSegment;
...@@ -341,4 +342,23 @@ public final class DataAdapter { ...@@ -341,4 +342,23 @@ public final class DataAdapter {
} }
return boms; 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; package com.aps.poa.data;
import java.util.Collections;
import java.util.List;
/** /**
* 库存策略:时变库存模型的物料参数。 * 库存策略:时变库存模型的物料参数。
*/ */
...@@ -9,13 +12,15 @@ public class InventoryPolicy { ...@@ -9,13 +12,15 @@ public class InventoryPolicy {
public final double safetyStock; public final double safetyStock;
public final double minStock; public final double minStock;
public final double maxStock; public final double maxStock;
public final double scheduledReceipts; /** 在途到货(分时段):每笔带各自到货时间(相对 timeZero 的分钟偏移) */
public final List<ScheduledReceipt> scheduledReceipts;
public final double lotSize; public final double lotSize;
public final int maxPurchaseLotsPerBucket; public final int maxPurchaseLotsPerBucket;
public final long holdingCostPerUnitPerBucket; public final long holdingCostPerUnitPerBucket;
public InventoryPolicy(String materialId, double onHand, double safetyStock, 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, double lotSize, int maxPurchaseLotsPerBucket,
long holdingCostPerUnitPerBucket) { long holdingCostPerUnitPerBucket) {
if (minStock > safetyStock || safetyStock > maxStock) { if (minStock > safetyStock || safetyStock > maxStock) {
...@@ -27,12 +32,24 @@ public class InventoryPolicy { ...@@ -27,12 +32,24 @@ public class InventoryPolicy {
this.safetyStock = safetyStock; this.safetyStock = safetyStock;
this.minStock = minStock; this.minStock = minStock;
this.maxStock = maxStock; this.maxStock = maxStock;
this.scheduledReceipts = scheduledReceipts; this.scheduledReceipts = scheduledReceipts != null ? scheduledReceipts : Collections.emptyList();
this.lotSize = lotSize; this.lotSize = lotSize;
this.maxPurchaseLotsPerBucket = maxPurchaseLotsPerBucket; this.maxPurchaseLotsPerBucket = maxPurchaseLotsPerBucket;
this.holdingCostPerUnitPerBucket = holdingCostPerUnitPerBucket; 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, public InventoryPolicy(String materialId, double onHand, double safetyStock,
double minStock, double maxStock, double scheduledReceipts, double minStock, double maxStock, double scheduledReceipts,
double lotSize) { double lotSize) {
......
...@@ -22,6 +22,8 @@ public class ModelConfig { ...@@ -22,6 +22,8 @@ public class ModelConfig {
public double weightHoldingCost = 1.0; public double weightHoldingCost = 1.0;
/** 安全库存短缺惩罚权重(宏排产) */ /** 安全库存短缺惩罚权重(宏排产) */
public double weightSafetyShortfall = 1000.0; public double weightSafetyShortfall = 1000.0;
/** 超产惩罚权重(宏排产):生产量超出需求时按超出量惩罚,驱动按需生产;与安全库存短缺同量级(超产/短缺 1:1 权衡) */
public double weightOverProduction = 1000.0;
public double maxHorizon = 10000.0; public double maxHorizon = 10000.0;
/** 当前排产时刻(相对 timeZero 的分钟偏移):所有工序 start ≥ now 硬下界,防止排到过去 */ /** 当前排产时刻(相对 timeZero 的分钟偏移):所有工序 start ≥ now 硬下界,防止排到过去 */
public double now = 0.0; 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 { ...@@ -41,6 +41,12 @@ public class OpDef {
public final String bufferId; public final String bufferId;
/** WIP 缓冲区容量(同时最多等待的工件数),bufferId 非空时有效 */ /** WIP 缓冲区容量(同时最多等待的工件数),bufferId 非空时有效 */
public final int bufferCapacity; public final int bufferCapacity;
/** 最小生产量(工艺级,0 表示不额外约束,实际下界为 max(需求, 此值)) */
public final double minProductionQty;
/** 最大生产量(工艺级,0 表示不额外约束) */
public final double maxProductionQty;
/** 批量倍数(工单数量必须是该数值的整数倍,1 表示无批量约束) */
public final double lotMultiplier;
private OpDef(Builder b) { private OpDef(Builder b) {
this.id = b.id; this.id = b.id;
...@@ -60,6 +66,9 @@ public class OpDef { ...@@ -60,6 +66,9 @@ public class OpDef {
this.maxWaitAfter = b.maxWaitAfter; this.maxWaitAfter = b.maxWaitAfter;
this.bufferId = b.bufferId; this.bufferId = b.bufferId;
this.bufferCapacity = b.bufferCapacity; this.bufferCapacity = b.bufferCapacity;
this.minProductionQty=b.minProductionQty;
this.maxProductionQty=b.maxProductionQty;
this.lotMultiplier=b.lotMultiplier;
} }
public static Builder builder(String id, String name) { public static Builder builder(String id, String name) {
...@@ -89,6 +98,12 @@ public class OpDef { ...@@ -89,6 +98,12 @@ public class OpDef {
String bufferId = null; String bufferId = null;
int bufferCapacity = 0; int bufferCapacity = 0;
double minProductionQty;
/** 最大生产量(工艺级,0 表示不额外约束) */
double maxProductionQty;
/** 批量倍数(工单数量必须是该数值的整数倍,1 表示无批量约束) */
double lotMultiplier = 1;
Builder(String id, String name) { Builder(String id, String name) {
this.id = id; this.id = id;
this.name = name; this.name = name;
...@@ -122,6 +137,9 @@ public class OpDef { ...@@ -122,6 +137,9 @@ public class OpDef {
public Builder buffer(String bufferId, int capacity) { public Builder buffer(String bufferId, int capacity) {
this.bufferId = bufferId; this.bufferCapacity = capacity; return this; 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() { public OpDef build() {
if (candidateResources.isEmpty()) { if (candidateResources.isEmpty()) {
......
...@@ -7,7 +7,7 @@ package com.aps.poa.data; ...@@ -7,7 +7,7 @@ package com.aps.poa.data;
* 分别指向该订单的首/末工序。</p> * 分别指向该订单的首/末工序。</p>
* *
* <p>宏排产字段(可选):{@link #materialId} 表示该订单生产的物料, * <p>宏排产字段(可选):{@link #materialId} 表示该订单生产的物料,
* {@link #maxProductionQty}/{@link #productionLotSize} 让生产数量成为 CP-SAT 决策变量; * {@link #quantity} 为需求量;生产批量/最小最大生产量定义在工艺工序(OpDef)上;
* {@link #planDirection} 支持成品正排 + 半成品倒排。</p> * {@link #planDirection} 支持成品正排 + 半成品倒排。</p>
*/ */
public class OrderDef { public class OrderDef {
...@@ -27,12 +27,6 @@ public class OrderDef { ...@@ -27,12 +27,6 @@ public class OrderDef {
public final String materialId; public final String materialId;
/** 需求量 */ /** 需求量 */
public final double quantity; public final double quantity;
/** 最小生产量(≥ quantity) */
public final double minProductionQty;
/** 最大生产量(受最大库存限制) */
public final double maxProductionQty;
/** 生产批量 */
public final double productionLotSize;
/** 排程方向(成品正排 / 半成品倒排) */ /** 排程方向(成品正排 / 半成品倒排) */
public final PlanDirection planDirection; public final PlanDirection planDirection;
/** 父订单 ID(半成品归属的成品订单) */ /** 父订单 ID(半成品归属的成品订单) */
...@@ -45,14 +39,13 @@ public class OrderDef { ...@@ -45,14 +39,13 @@ public class OrderDef {
public OrderDef(String orderId, String firstOpId, String lastOpId, public OrderDef(String orderId, String firstOpId, String lastOpId,
double release, double due) { double release, double due) {
this(orderId, firstOpId, lastOpId, release, due, this(orderId, firstOpId, lastOpId, release, due,
null, 0, 0, 0, 0, PlanDirection.FORWARD, null, 0, PlanDirection.FORWARD,
null, null, 0); null, null, 0);
} }
public OrderDef(String orderId, String firstOpId, String lastOpId, public OrderDef(String orderId, String firstOpId, String lastOpId,
double release, double due, double release, double due,
String materialId, double quantity, String materialId, double quantity,
double minProductionQty, double maxProductionQty, double productionLotSize,
PlanDirection planDirection, PlanDirection planDirection,
String parentOrderId, String parentOperationId, double supplyLeadTime) { String parentOrderId, String parentOperationId, double supplyLeadTime) {
this.orderId = orderId; this.orderId = orderId;
...@@ -62,9 +55,6 @@ public class OrderDef { ...@@ -62,9 +55,6 @@ public class OrderDef {
this.due = due; this.due = due;
this.materialId = materialId; this.materialId = materialId;
this.quantity = quantity; this.quantity = quantity;
this.minProductionQty = minProductionQty;
this.maxProductionQty = maxProductionQty;
this.productionLotSize = productionLotSize;
this.planDirection = planDirection; this.planDirection = planDirection;
this.parentOrderId = parentOrderId; this.parentOrderId = parentOrderId;
this.parentOperationId = parentOperationId; 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; ...@@ -3,6 +3,7 @@ package com.aps.poa.model;
import com.aps.entity.basic.Entry; import com.aps.entity.basic.Entry;
import com.aps.entity.basic.Machine; import com.aps.entity.basic.Machine;
import com.aps.poa.constraint.ConstraintFactory; import com.aps.poa.constraint.ConstraintFactory;
import com.aps.poa.data.BatchSplitter;
import com.aps.poa.data.BomItemDef; import com.aps.poa.data.BomItemDef;
import com.aps.poa.data.BomRelation; import com.aps.poa.data.BomRelation;
import com.aps.poa.data.DataAdapter; import com.aps.poa.data.DataAdapter;
...@@ -104,6 +105,10 @@ public class POAOrToolsModel { ...@@ -104,6 +105,10 @@ public class POAOrToolsModel {
private final Map<String, IntVar> productionLotCountVars = new LinkedHashMap<>(); private final Map<String, IntVar> productionLotCountVars = new LinkedHashMap<>();
/** 每个物料的时变库存 Inventory[t](宏排产) */ /** 每个物料的时变库存 Inventory[t](宏排产) */
private final Map<String, IntVar[]> inventoryVars = new LinkedHashMap<>(); 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; private IntVar totalInventoryHoldingCost;
/** 安全库存短缺总和(进目标) */ /** 安全库存短缺总和(进目标) */
...@@ -190,6 +195,16 @@ public class POAOrToolsModel { ...@@ -190,6 +195,16 @@ public class POAOrToolsModel {
List<SetupMatrix> setupMatrices, List<SetupMatrix> setupMatrices,
List<InventoryConstraint> invConstraints, List<InventoryConstraint> invConstraints,
ModelConfig config) { 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), this(DataAdapter.toOpDefs(entries),
DataAdapter.toResourceDefs(machines, config != null ? config.timeZero : null), DataAdapter.toResourceDefs(machines, config != null ? config.timeZero : null),
DataAdapter.toBomRelations(entries), DataAdapter.toBomRelations(entries),
...@@ -300,6 +315,8 @@ public class POAOrToolsModel { ...@@ -300,6 +315,8 @@ public class POAOrToolsModel {
public Map<String, IntVar> getProductionQtyVars() { return productionQtyVars; } public Map<String, IntVar> getProductionQtyVars() { return productionQtyVars; }
public Map<String, IntVar> getProductionLotCountVars() { return productionLotCountVars; } public Map<String, IntVar> getProductionLotCountVars() { return productionLotCountVars; }
public Map<String, IntVar[]> getInventoryVars() { return inventoryVars; } 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 IntVar getTotalInventoryHoldingCost() { return totalInventoryHoldingCost; }
public void setTotalInventoryHoldingCost(IntVar v) { this.totalInventoryHoldingCost = v; } public void setTotalInventoryHoldingCost(IntVar v) { this.totalInventoryHoldingCost = v; }
public IntVar getTotalSafetyStockShortfall() { return totalSafetyStockShortfall; } public IntVar getTotalSafetyStockShortfall() { return totalSafetyStockShortfall; }
......
...@@ -146,15 +146,29 @@ public final class ObjectiveBuilder { ...@@ -146,15 +146,29 @@ public final class ObjectiveBuilder {
} }
} }
/** 库存持有成本 + 安全库存短缺(宏排产,进目标) */ /** 库存持有成本 + 安全库存短缺 + 超产惩罚(宏排产,进目标) */
private static void addInventoryCostTerms(POAOrToolsModel model) { 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) { if (model.getTotalInventoryHoldingCost() != null) {
model.getObjectiveVars().add(model.getTotalInventoryHoldingCost()); model.getObjectiveVars().add(model.getTotalInventoryHoldingCost());
model.getObjectiveCoeffs().add(model.lscale(model.getConfig().weightHoldingCost)); model.getObjectiveCoeffs().add(holdingCoeff);
} }
if (model.getTotalSafetyStockShortfall() != null) { if (model.getTotalSafetyStockShortfall() != null) {
model.getObjectiveVars().add(model.getTotalSafetyStockShortfall()); 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);
} }
} }
} }
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