Commit bf94442c authored by Tong Li's avatar Tong Li

POA

parent 24eee50e
This diff is collapsed.
......@@ -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;
/**
* 所需物料
......
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);
}
}
}
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