Commit 58128f3f authored by Tong Li's avatar Tong Li

MP

parent b3685a77
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -32,7 +32,7 @@ import java.util.stream.Collectors;
* <tr><td>POST</td><td>/macroResult/run?sceneId=xxx</td><td>触发排产优化: 转换→验证→建模→分层求解→保存JSON</td></tr>
* <tr><td>GET </td><td>/macroResult/salesDemands?sceneId=xxx</td><td>订单级需求满足明细</td></tr>
* <tr><td>GET </td><td>/macroResult/supplyChain?sceneId=xxx</td><td>产品级库存流转与销售满足汇总</td></tr>
* <tr><td>GET </td><td>/macroResult/productSummary?sceneId=xxx</td><td>产品-库位跨周期库存汇总</td></tr>
* <tr><td>GET </td><td>/macroResult/productSummary?sceneId=xxx</td><td>产品-库位库存周期明细(不聚合)</td></tr>
* <tr><td>GET </td><td>/macroResult/unitCapacity?sceneId=xxx</td><td>设备产能使用情况(含利用率)</td></tr>
* <tr><td>GET </td><td>/macroResult/productNetwork?sceneId=xxx</td><td>BOM生产网络</td></tr>
* <tr><td>GET </td><td>/macroResult/summary?sceneId=xxx</td><td>KPI与求解统计</td></tr>
......@@ -42,7 +42,7 @@ import java.util.stream.Collectors;
* <ol>
* <li>POST /run → 触发求解, 返回 status:"SUCCESS"</li>
* <li>GET /salesDemands → 查看订单需求满足</li>
* <li>GET /supplyChain → 查看库存与销售满足</li>
* <li>GET /productSummary → 查看库存周期明细</li>
* <li>GET /unitCapacity → 查看设备产能使用</li>
* <li>GET /productNetwork → 查看BOM网络</li>
* <li>GET /summary → 查看KPI与求解统计</li>
......@@ -163,30 +163,30 @@ public class MacroPlannerResultController {
}
/**
* 获取产品级库存汇总: 按 (productId, spId) 聚合跨周期库存流转。
* 获取产品级库存明细: 按 (productId, spId) 分组, 保留每周期原始库存数据, 不跨周期聚合。
*
* <h3>返回字段说明</h3>
* <table>
* <tr><td>totalCount</td><td>产品-库位总数</td></tr>
* <tr><td>totalCount</td><td>产品-库位组合总数</td></tr>
* <tr><td>totalPispipRecords</td><td>库存点库存原始记录数</td></tr>
* <tr><td>summaries</td><td>按产品+库位聚合的库存汇总</td></tr>
* <tr><td>summaries</td><td>按产品+库位分组的库存数据</td></tr>
* <tr><td>summaries[].key</td><td>分组键: productId@spId</td></tr>
* <tr><td>summaries[].productId / spId</td><td>产品ID / 库存点ID</td></tr>
* <tr><td>summaries[].periodCount</td><td>周期数</td></tr>
* <tr><td>summaries[].initialInventory</td><td>首周期期初库存</td></tr>
* <tr><td>summaries[].finalInventory</td><td>末周期期末库存</td></tr>
* <tr><td>summaries[].totalInflow</td><td>总流入(生产到货+在途到货)</td></tr>
* <tr><td>summaries[].totalOutflow</td><td>总流出(销售消耗+BOM依赖消耗)</td></tr>
* <tr><td>summaries[].totalProduction</td><td>生产到货合计</td></tr>
* <tr><td>summaries[].totalInTransit</td><td>在途到货合计</td></tr>
* <tr><td>summaries[].belowTarget</td><td>低于目标库存累计值</td></tr>
* <tr><td>summaries[].belowMin / aboveMax</td><td>违反最小/最大库存惩罚</td></tr>
* <tr><td>summaries[].periodCount</td><td>该产品-库位的周期数</td></tr>
* <tr><td>summaries[].records</td><td>PispipResult[] 每周期原始明细</td></tr>
* <tr><td>records[].periodIndex</td><td>周期索引</td></tr>
* <tr><td>records[].openingInventory / endingInventory</td><td>期初/期末库存</td></tr>
* <tr><td>records[].totalInflow / totalOutflow</td><td>流入/流出</td></tr>
* <tr><td>records[].productionArrived</td><td>生产到货量</td></tr>
* <tr><td>records[].inTransitArrival</td><td>在途到货量</td></tr>
* <tr><td>records[].belowTarget / belowMin / aboveMax</td><td>库存偏差</td></tr>
* </table>
*/
@GetMapping("/productSummary")
@Operation(summary = "产品库存汇总",
description = "按产品+库位聚合跨周期库存汇总: 期初/期末库存、总流入/流出、生产到货、库存偏差。"
+ "数据来源: PispipResult(库存点库存)。"
+ "注: 如需订单级需求满足明细, 请使用 /salesDemands 接口。")
@Operation(summary = "产品库存明细",
description = "按产品+库位分组, 保留每周期原始PispipResult明细, 不跨周期聚合。"
+ "与 supplyChain 中的 productSummaries (跨周期求和) 不同, "
+ "此接口返回每周期独立数据, 适合前端展开查看各周期详情。")
public R<Map<String, Object>> getProductSummary(
@RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId) {
OptimizationResult result = loadResult(sceneId);
......@@ -195,12 +195,31 @@ public class MacroPlannerResultController {
}
List<PispipResult> pispips = result.getPispips();
List<Map<String, Object>> summaries = buildProductSummaries(pispips);
// 按 productId@spId 分组,保留每周期明细,不跨周期聚合
Map<String, List<PispipResult>> grouped = pispips != null
? pispips.stream().collect(Collectors.groupingBy(
p -> p.getProductId() + "@" + p.getSpId(),
LinkedHashMap::new,
Collectors.toList()))
: new LinkedHashMap<>();
List<Map<String, Object>> summaries = new ArrayList<>();
for (Map.Entry<String, List<PispipResult>> entry : grouped.entrySet()) {
List<PispipResult> records = entry.getValue();
Map<String, Object> s = new LinkedHashMap<>();
s.put("key", entry.getKey());
s.put("productId", records.get(0).getProductId());
s.put("spId", records.get(0).getSpId());
s.put("periodCount", records.size());
s.put("records", records); // 每周期明细, 不聚合
summaries.add(s);
}
Map<String, Object> data = new LinkedHashMap<>();
data.put("totalCount", summaries.size());
data.put("PispipRecords", pispips);
//data.put("summaries", summaries);
data.put("totalPispipRecords", pispips != null ? pispips.size() : 0);
data.put("summaries", summaries);
return R.ok(data);
}
......
package com.aps.macroplanner;
import com.aps.macroplanner.MacroPlannerOptimizer;
import com.google.ortools.Loader;
import com.aps.macroplanner.data.ComprehensiveTestDataBuilder;
/**
* 综合测试运行器 — 使用覆盖所有约束场景的测试数据运行优化器。
*
* <p>与 {@link MacroPlannerOptimizer} 使用相同的模块架构,
* 仅替换数据源为 {@link ComprehensiveTestDataBuilder}。</p>
*
* <h2>测试场景</h2>
* 3级BOM + 2设备 + 5周期 + 需求波动 + 紧约束:
* <ul>
* <li>物料平衡 — 3级链式传递, 5周期</li>
* <li>BOM依赖 — OP1双输入, PC共享消耗</li>
* <li>产能 — Unit1共享(OP1+OP3), Unit2最小产能30</li>
* <li>库存 — PC最大库存80紧约束</li>
* <li>供应 — 目标偏高</li>
* <li>批次 — OP1 lotSize=100, OP2 lotSize=50</li>
* </ul>
*/
public class ComprehensiveTestRunner {
public static void main(String[] args) {
Loader.loadNativeLibraries();
System.out.println("╔══════════════════════════════════════════════════════╗");
System.out.println("║ MacroPlanner 综合测试 — 覆盖所有约束场景 ║");
System.out.println("║ 3级BOM + 2设备 + 5周期 + 需求波动 + 紧约束 ║");
System.out.println("╚══════════════════════════════════════════════════════╝\n");
MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(
new ComprehensiveTestDataBuilder());
optimizer.buildModel();
optimizer.solve();
}
}
\ No newline at end of file
package com.aps.macroplanner.constraint;
import com.google.ortools.linearsolver.MPConstraint;
import com.google.ortools.linearsolver.MPVariable;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
import java.util.Map;
/**
* 物料平衡约束构建器 (Balance)
*
* <p>确保每个 PISPIP (Product In StockingPoint In Period) 的物料流守恒:
* 流入 = 流出</p>
*
* <h3>数学公式</h3>
* <pre>
* Σ(PTQty×relDur) + DemandSlack + InvQty(t-1) + 在途到货
* - ΣSalesDemandQty - DependentDemand - InvQty(t) = 初始库存(t=0)
* </pre>
*/
public class BalanceConstraint {
/**
* 构建物料平衡约束。
*/
public static void build(MacroPlannerModel model, TestDataBuilder data) {
Map<String, MPVariable> ptQtyVars = model.getPtQtyVars();
Map<String, MPVariable> invQtyVars = model.getInvQtyVars();
Map<String, MPVariable> salesDemandQtyVars = model.getSalesDemandQtyVars();
Map<String, MPVariable> demandSlackVars = model.getDemandSlackVars();
Map<String, MPVariable> dependentDemandVars = model.getDependentDemandVars();
for (Product prod : data.getProducts()) {
for (StockingPoint sp : data.getStockingPointsForProduct(prod.getId())) {
for (Period p : data.getPeriods()) {
String invKey = prod.getId() + "_" + sp.getId() + "_" + p.getIndex();
MPConstraint balance = model.getSolver().makeConstraint(0.0, 0.0,
"Balance_" + prod.getId() + "_" + sp.getId() + "_P" + p.getIndex());
// === 流入 ===
// PTQty 产出 (考虑提前期: productionDate 生产, productionDate+leadTimeDays 到货)
// 关键: 只统计产出到当前库存点(sp)的操作, 避免多工序路由中产出到错误的库存点
for (Operation op : data.getOperations()) {
if (!op.producesProductAtSp(prod.getId(), sp.getId())) continue;
int leadTimeDays = op.getLeadTimeDays();
Period productionPeriod = data.getPeriodOffsetByDays(p, leadTimeDays);
if (productionPeriod == null) continue;
for (UnitOperation uo : op.getUnitOperations()) {
MPVariable ptVar = ptQtyVars.get(op.ptQtyKey(uo, productionPeriod.getIndex()));
if (ptVar != null) balance.setCoefficient(ptVar, op.getRelativeDuration());
}
}
// DemandSlack
MPVariable slackVar = demandSlackVars.get(invKey);
if (slackVar != null) balance.setCoefficient(slackVar, 1.0);
// 上一周期库存 (t>0)
if (p.getIndex() > 0) {
String prevKey = prod.getId() + "_" + sp.getId() + "_" + (p.getIndex() - 1);
MPVariable prevVar = invQtyVars.get(prevKey);
if (prevVar != null) balance.setCoefficient(prevVar, 1.0);
}
// === 流出 ===
// SalesDemandQty
for (SalesDemand sd : data.getSalesDemandsFor(prod, sp, p)) {
MPVariable sdVar = salesDemandQtyVars.get(sd.getKey());
if (sdVar != null) balance.setCoefficient(sdVar, -1.0);
}
// DependentDemandInPISPIP
MPVariable ddVar = dependentDemandVars.get(invKey);
if (ddVar != null) balance.setCoefficient(ddVar, -1.0);
// InvQty
MPVariable invVar = invQtyVars.get(invKey);
if (invVar != null) balance.setCoefficient(invVar, -1.0);
// === RHS ===
double rhs = 0.0;
// 初始库存 (t=0)
if (p.getIndex() == 0) {
rhs -= data.getInitialInventory(prod.getId(), sp.getId());
}
// 在途供应 (供应商已发货, 固定到货量, 按日期匹配周期)
for (InTransitSupply its : data.getInTransitSupplies()) {
if (its.getProduct().getId().equals(prod.getId())
&& its.getStockingPoint().getId().equals(sp.getId())
&& p.equals(data.getPeriodByDate(its.getArrivalDate()))) {
rhs -= its.getQuantity();
}
}
balance.setBounds(rhs, rhs);
}
}
}
}
}
\ No newline at end of file
package com.aps.macroplanner.constraint;
import com.google.ortools.linearsolver.MPConstraint;
import com.google.ortools.linearsolver.MPVariable;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
import java.util.Map;
/**
* BOM 依赖需求约束构建器 (BomConstraint)
*
* <p>建模多级供应链中的物料消耗关系:
* 生产成品需要消耗原材料/半成品 (BOM 展开)。</p>
*
* <h3>约束 1: OperationDemandQty = PTQty × BOM 因子</h3>
* <h3>约束 2: DependentDemandInPISPIP = Σ OperationDemandQty</h3>
*/
public class BomConstraint {
/**
* 构建 BOM 依赖需求约束。
*/
public static void build(MacroPlannerModel model, TestDataBuilder data) {
Map<String, MPVariable> ptQtyVars = model.getPtQtyVars();
Map<String, MPVariable> operationDemandQtyVars = model.getOperationDemandQtyVars();
Map<String, MPVariable> dependentDemandVars = model.getDependentDemandVars();
// --- 约束 1: OperationDemandQty = input.Factor × ΣPTQty(跨所有Unit) ---
for (OperationInput input : data.getOperationInputs()) {
Operation op = input.getOperation();
for (Period p : data.getPeriods()) {
String odKey = input.getKey() + "_" + p.getIndex();
MPVariable odVar = operationDemandQtyVars.get(odKey);
if (odVar == null) continue;
MPConstraint odd = model.getSolver().makeConstraint(0.0, 0.0,
"OpDemandDef_" + odKey);
odd.setCoefficient(odVar, 1.0);
for (UnitOperation uo : op.getUnitOperations()) {
MPVariable ptVar = ptQtyVars.get(op.ptQtyKey(uo, p.getIndex()));
if (ptVar != null) odd.setCoefficient(ptVar, -input.getFactor());
}
}
}
// --- 约束 2: DependentDemandInPISPIP = Σ OperationDemandQty ---
for (Product prod : data.getProducts()) {
for (StockingPoint sp : data.getStockingPointsForProduct(prod.getId())) {
for (Period p : data.getPeriods()) {
String ddKey = prod.getId() + "_" + sp.getId() + "_" + p.getIndex();
MPVariable ddVar = dependentDemandVars.get(ddKey);
if (ddVar == null) continue;
MPConstraint ddCon = model.getSolver().makeConstraint(0.0, 0.0,
"DepDemandDef_" + ddKey);
ddCon.setCoefficient(ddVar, -1.0);
for (OperationInput input : data.getOperationInputs()) {
if (!input.getInputProduct().getId().equals(prod.getId())) continue;
if (!input.getInputSp().getId().equals(sp.getId())) continue;
MPVariable odVar = operationDemandQtyVars.get(
input.getKey() + "_" + p.getIndex());
if (odVar != null) ddCon.setCoefficient(odVar, 1.0);
}
}
}
}
}
}
\ No newline at end of file
package com.aps.macroplanner.constraint;
import com.google.ortools.linearsolver.MPConstraint;
import com.google.ortools.linearsolver.MPSolver;
import com.google.ortools.linearsolver.MPVariable;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
import java.util.Map;
/**
* 产能约束构建器 (CapacityConstraint)
*
* <p>确保每个设备在每个周期的产能使用量在 [MinCapacity, MaxCapacity] 范围内。</p>
*
* <h3>数学公式</h3>
* <pre>
* 最大产能: Σ(PTQty × coeff) - CapacityOverloaded ≤ MaxCapacity
* 最小产能: Σ(PTQty × coeff) + CapacityNotMet ≥ MinCapacity
* </pre>
*/
public class CapacityConstraint {
/**
* 构建产能上下限约束。
*/
public static void build(MacroPlannerModel model, TestDataBuilder data) {
Map<String, MPVariable> ptQtyVars = model.getPtQtyVars();
Map<String, MPVariable> overloadVars = model.getCapacityOverloadedVars();
Map<String, MPVariable> notMetVars = model.getCapacityNotMetVars();
for (UnitPeriod up : data.getUnitPeriods()) {
String capKey = up.getKey();
// --- 最大产能 ---
MPConstraint maxCap = model.getSolver().makeConstraint(
-MPSolver.infinity(), up.getMaxCapacity(), "MaxCap_" + capKey);
for (Operation op : data.getOperations()) {
for (UnitOperation uo : op.getUnitOperations()) {
if (!uo.getUnitId().equals(up.getUnitId())) continue;
MPVariable ptVar = ptQtyVars.get(op.ptQtyKey(uo, up.getPeriod().getIndex()));
if (ptVar != null) maxCap.setCoefficient(ptVar, uo.getCapacityCoeff());
}
}
MPVariable ov = overloadVars.get(capKey);
if (ov != null) maxCap.setCoefficient(ov, -1.0);
// --- 最小产能 ---
if (up.hasMinCapacity()) {
MPConstraint minCap = model.getSolver().makeConstraint(
up.getMinCapacity(), MPSolver.infinity(), "MinCap_" + capKey);
for (Operation op : data.getOperations()) {
for (UnitOperation uo : op.getUnitOperations()) {
if (!uo.getUnitId().equals(up.getUnitId())) continue;
MPVariable ptVar = ptQtyVars.get(op.ptQtyKey(uo, up.getPeriod().getIndex()));
if (ptVar != null) minCap.setCoefficient(ptVar, uo.getCapacityCoeff());
}
}
MPVariable nm = notMetVars.get(capKey);
if (nm != null) minCap.setCoefficient(nm, 1.0);
}
}
}
}
\ No newline at end of file
package com.aps.macroplanner.constraint;
import com.aps.macroplanner.data.TestDataBuilder;
import com.aps.macroplanner.model.MacroPlannerModel;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* 约束工厂 — 统一调度所有约束的构建。
*
* <p>设计原则: 与 {@link com.aps.macroplanner.model.VariableFactory} 对应,
* 提供单一入口 {@link #buildAll(MacroPlannerModel, TestDataBuilder)},
* 按逻辑顺序逐个委托给各约束构建器。</p>
*
* <h3>构建顺序 (必须严格遵循)</h3>
* <pre>
* 1. 物料平衡约束 — 核心约束, 确保流入=流出
* 2. BOM 依赖需求 — 依赖 PTQty 变量, 定义物料消耗关系
* 3. 需求满足量汇总 — 依赖 SalesDemandQty + OperationDemandQty
* 4. 需求缺口联动 — 强制 SalesDemandQty + DemandSlack >= DemandQuantity
* 5. 库存规格约束 — 依赖 InvQty + DemandFulfillment (in-days 模式)
* 6. 产能约束 — 依赖 PTQty 变量, 限制设备使用
* 7. 供应规格约束 — 依赖 PTQty 变量, 管理总供应量
* 8. 批次大小约束 — 依赖 PTQty 变量, 控制批量生产
* 9. KPI 汇总变量 — 依赖所有松弛变量, 汇总到目标函数
* </pre>
*
* <p>注意: 各约束构建器之间通过 {@link MacroPlannerModel} 共享变量,
* 因此构建顺序相对独立, 但建议按上述顺序以确保可读性。</p>
*/
public class ConstraintFactory {
private static final Logger LOG = Logger.getLogger(ConstraintFactory.class.getName());
/**
* 构建所有约束和 KPI 汇总变量。
*
* <p>每个步骤前后记录约束数量的变化, 便于验证每个构建器是否生效。</p>
*
* @param model 模型容器 (提供求解器和所有变量)
* @param data 测试数据 (提供索引维度和规格参数)
*/
public static void buildAll(MacroPlannerModel model, TestDataBuilder data) {
LOG.info("========== 开始构建约束 ==========");
int beforeConstraints = model.getSolver().numConstraints();
int beforeVariables = model.getSolver().numVariables();
// 1. 物料平衡约束 (核心约束, 必须最先创建)
BalanceConstraint.build(model, data);
logStep("1. 物料平衡", "确保每个 PISPIP 流入 = 流出",
model, beforeConstraints, beforeVariables);
beforeConstraints = model.getSolver().numConstraints();
beforeVariables = model.getSolver().numVariables();
// 2. BOM 依赖需求约束 (多级物料消耗)
BomConstraint.build(model, data);
logStep("2. BOM依赖需求", "OperationDemandQty = PTQty × BOM因子",
model, beforeConstraints, beforeVariables);
beforeConstraints = model.getSolver().numConstraints();
beforeVariables = model.getSolver().numVariables();
// 3. 需求满足量汇总 (安全库存天数计算的基础)
DemandFulfillmentConstraint.build(model, data);
logStep("3. 需求满足量汇总", "DemandFulfillment = SalesDemandQty + OperationDemandQty",
model, beforeConstraints, beforeVariables);
beforeConstraints = model.getSolver().numConstraints();
beforeVariables = model.getSolver().numVariables();
// 4. 需求缺口联动 (强制 SalesDemandQty + DemandSlack >= DemandQuantity)
DemandSlackLinkageConstraint.build(model, data);
logStep("4. 需求缺口联动", "SalesDemandQty + DemandSlack >= DemandQuantity",
model, beforeConstraints, beforeVariables);
beforeConstraints = model.getSolver().numConstraints();
beforeVariables = model.getSolver().numVariables();
// 4.5 工序产量一致性 (强制 Routing 内相邻工序 PTQty 相等, 防止 WIP 堆积)
RoutingConstraint.build(model, data);
logStep("4.5 工序产量一致", "Routing 内相邻工序 PTQty 相等, 防止 WIP 堆积",
model, beforeConstraints, beforeVariables);
beforeConstraints = model.getSolver().numConstraints();
beforeVariables = model.getSolver().numVariables();
// 5. 库存规格约束 (最小/最大/目标库存)
InventorySpecConstraint.build(model, data);
logStep("5. 库存规格", "InvQty + Slack ≥ Min/Max/Target (支持天数模式)",
model, beforeConstraints, beforeVariables);
beforeConstraints = model.getSolver().numConstraints();
beforeVariables = model.getSolver().numVariables();
// 6. 产能约束 (设备产能上下限)
CapacityConstraint.build(model, data);
logStep("6. 产能约束", "Σ(PTQty×coeff) ∈ [MinCapacity, MaxCapacity]",
model, beforeConstraints, beforeVariables);
beforeConstraints = model.getSolver().numConstraints();
beforeVariables = model.getSolver().numVariables();
// 7. 供应规格约束 (跨周期总量供应)
SupplySpecConstraint.build(model, data);
logStep("7. 供应规格", "ΣPTQty + Slack ≥ Target/Min/Max",
model, beforeConstraints, beforeVariables);
beforeConstraints = model.getSolver().numConstraints();
beforeVariables = model.getSolver().numVariables();
// 8. 批次大小约束 (批量生产)
LotSizeConstraint.build(model, data);
logStep("8. 批次大小", "PTQty×QTPFactor + Slack ∈ [LotSize]",
model, beforeConstraints, beforeVariables);
beforeConstraints = model.getSolver().numConstraints();
beforeVariables = model.getSolver().numVariables();
// 9. KPI 汇总变量及定义约束 (必须在目标函数之前)
KpiAggregator.build(model, data);
logStep("9. KPI汇总", "TotalKPI = Σ 松弛变量 (12个KPI)",
model, beforeConstraints, beforeVariables);
logSummary(model);
}
/**
* 记录单个约束构建步骤的结果。
* 输出新增约束数和新增变量数, 如果为 0 则输出 WARNING。
*/
private static void logStep(String stepName, String description,
MacroPlannerModel model,
int beforeConstraints, int beforeVariables) {
int addedConstraints = model.getSolver().numConstraints() - beforeConstraints;
int addedVariables = model.getSolver().numVariables() - beforeVariables;
if (addedConstraints == 0 && addedVariables == 0) {
LOG.warning(String.format("[%s] %s → 未创建任何约束或变量! (可能数据为空)",
stepName, description));
} else {
StringBuilder sb = new StringBuilder();
sb.append(String.format("[%s] %s", stepName, description));
if (addedConstraints > 0) {
sb.append(String.format(" | +%d约束", addedConstraints));
}
if (addedVariables > 0) {
sb.append(String.format(" | +%d变量", addedVariables));
}
LOG.info(sb.toString());
}
}
/**
* 输出构建完成汇总。
*/
private static void logSummary(MacroPlannerModel model) {
int totalConstraints = model.getSolver().numConstraints();
int totalVariables = model.getSolver().numVariables();
LOG.info(String.format("========== 约束构建完成: 总计 %d 约束, %d 变量 ==========",
totalConstraints, totalVariables));
}
}
\ No newline at end of file
package com.aps.macroplanner.constraint;
import com.google.ortools.linearsolver.MPConstraint;
import com.google.ortools.linearsolver.MPVariable;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
import java.util.Map;
/**
* 需求满足量汇总约束 (DemandFulfillmentInPISPIP)
*
* <p>定义每个 PISPIP 的总需求满足量 = 销售需求 + BOM 依赖需求。
* 这个变量是安全库存天数计算的基础。</p>
*
* <h3>数学公式</h3>
* <pre>
* DemandFulfillmentInPISPIP = Σ SalesDemandQty + Σ OperationDemandQty
*
* 即: 每个 PISPIP 的总需求满足量 = 所有销售需求满足量 + 所有 BOM 消耗量
* </pre>
*
* <h3>用途</h3>
* 安全库存天数约束: InvQty ≥ Σ(ratio × DemandFulfillment)
* 即: 期末库存必须覆盖未来N天的需求满足量
*/
public class DemandFulfillmentConstraint {
/**
* 构建 DemandFulfillmentInPISPIP 定义约束。
*/
public static void build(MacroPlannerModel model, TestDataBuilder data) {
Map<String, MPVariable> dfVars = model.getDemandFulfillmentVars();
Map<String, MPVariable> sdVars = model.getSalesDemandQtyVars();
Map<String, MPVariable> opDemandVars = model.getOperationDemandQtyVars();
for (Product prod : data.getProducts()) {
for (StockingPoint sp : data.getStockingPointsForProduct(prod.getId())) {
for (Period p : data.getPeriods()) {
String key = prod.getId() + "_" + sp.getId() + "_" + p.getIndex();
// DemandFulfillment = SalesDemandQty + OperationDemandQty
MPConstraint c = model.getSolver().makeConstraint(
0.0, 0.0, "DFulfill_" + key);
// + DemandFulfillment 变量
MPVariable dfVar = dfVars.get(key);
if (dfVar != null) c.setCoefficient(dfVar, -1.0);
// + SalesDemandQty
for (SalesDemand sd : data.getSalesDemands()) {
if (sd.getProduct().getId().equals(prod.getId())
&& sd.getStockingPoint().getId().equals(sp.getId())
&& sd.getPeriod().getIndex() == p.getIndex()) {
MPVariable sdVar = sdVars.get(sd.getKey());
if (sdVar != null) c.setCoefficient(sdVar, 1.0);
}
}
// + OperationDemandQty (BOM 消耗)
for (OperationInput input : data.getOperationInputs()) {
if (input.getInputProduct().getId().equals(prod.getId())
&& input.getInputSp().getId().equals(sp.getId())) {
String opKey = input.getKey() + "_" + p.getIndex();
MPVariable odVar = opDemandVars.get(opKey);
if (odVar != null) c.setCoefficient(odVar, 1.0);
}
}
}
}
}
}
}
\ No newline at end of file
package com.aps.macroplanner.constraint;
import com.google.ortools.linearsolver.MPConstraint;
import com.google.ortools.linearsolver.MPSolver;
import com.google.ortools.linearsolver.MPVariable;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
import com.aps.macroplanner.util.SafetyStockCalculator;
import com.aps.macroplanner.util.SafetyStockTerm;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
/**
* 库存规格约束构建器 (InventorySpecConstraint)
*
* <p>为每个产品/库存点/周期定义三个库存水平约束,
* 支持两种模式: 绝对数量模式 和 安全库存天数模式。</p>
*
* <h3>绝对数量模式</h3>
* <pre>
* 最小库存: InvQty + MinInvQtyUnder ≥ MinLevel (固定数量)
* 最大库存: InvQty - MaxInvQtyOver ≤ MaxLevel
* 目标库存: InvQty + InvQtyUnderTarget = TargetLevel
* </pre>
*
* <h3>安全库存天数模式</h3>
* <pre>
* 委托给 {@link SafetyStockCalculator} 进行折算。
* 最小库存: InvQty + MinInvQtyUnder ≥ Σ(ratio_i × DemandFulfillment_i)
* 最大库存: InvQty - MaxInvQtyOver ≤ Σ(ratio_i × DemandFulfillment_i)
* 目标库存: InvQty + InvQtyUnderTarget = Σ(ratio_i × DemandFulfillment_i)
* </pre>
*/
public class InventorySpecConstraint {
private static final Logger LOG = Logger.getLogger(InventorySpecConstraint.class.getName());
/**
* 构建库存规格约束 (自动判断绝对数量模式 vs 天数模式)。
*
* <p>在安全库存天数模式下, 会输出详细日志, 便于排查:
* <ul>
* <li>每个 PISPIP 的安全库存天数折算过程</li>
* <li>约束添加的 DemandFulfillment 项及其系数</li>
* <li>绝对数量模式下跳过的详细信息</li>
* </ul></p>
*/
public static void build(MacroPlannerModel model, TestDataBuilder data) {
Map<String, MPVariable> invVars = model.getInvQtyVars();
Map<String, MPVariable> minUnderVars = model.getMinInvQtyUnderVars();
Map<String, MPVariable> maxOverVars = model.getMaxInvQtyOverVars();
Map<String, MPVariable> targetUnderVars = model.getInvQtyUnderTargetVars();
Map<String, MPVariable> dfVars = model.getDemandFulfillmentVars();
List<Period> periods = data.getPeriods();
int inDaysCount = 0;
int absoluteCount = 0;
for (InventorySpec spec : data.getInventorySpecs()) {
String invKey = spec.getKey();
MPVariable invVar = invVars.get(invKey);
if (invVar == null) continue;
int periodIdx = spec.getPeriod().getIndex();
boolean usedInDays = false;
// === 最小库存 ===
if (spec.hasMinLevel()) {
if (spec.hasMinLevelInDays()) {
usedInDays = true;
// 安全库存天数模式: RHS=0, 由 DemandFulfillment 项提供 RHS
MPConstraint c = model.getSolver().makeConstraint(
0.0, MPSolver.infinity(), "MinInv_" + invKey);
c.setCoefficient(invVar, 1.0);
MPVariable u = minUnderVars.get(invKey);
if (u != null) c.setCoefficient(u, 1.0);
LOG.info(String.format("[MinInv_天] %s: 最小库存=%d天, 开始折算...",
invKey, (int) spec.getMinLevelInDays()));
addDemandFulfillmentTerms(c, invKey, dfVars, periods, periodIdx,
spec.getMinLevelInDays(), "MinInv");
} else {
// 绝对数量模式
double rhs = spec.getMinLevel();
MPConstraint c = model.getSolver().makeConstraint(
rhs, MPSolver.infinity(), "MinInv_" + invKey);
c.setCoefficient(invVar, 1.0);
MPVariable u = minUnderVars.get(invKey);
if (u != null) c.setCoefficient(u, 1.0);
absoluteCount++;
}
}
// === 最大库存 ===
if (spec.hasMaxLevel()) {
if (spec.hasMaxLevelInDays()) {
usedInDays = true;
MPConstraint c = model.getSolver().makeConstraint(
-MPSolver.infinity(), 0.0, "MaxInv_" + invKey);
c.setCoefficient(invVar, 1.0);
MPVariable o = maxOverVars.get(invKey);
if (o != null) c.setCoefficient(o, -1.0);
LOG.info(String.format("[MaxInv_天] %s: 最大库存=%d天",
invKey, (int) spec.getMaxLevelInDays()));
addDemandFulfillmentTerms(c, invKey, dfVars, periods, periodIdx,
spec.getMaxLevelInDays(), "MaxInv");
} else {
double rhs = spec.getMaxLevel();
MPConstraint c = model.getSolver().makeConstraint(
-MPSolver.infinity(), rhs, "MaxInv_" + invKey);
c.setCoefficient(invVar, 1.0);
MPVariable o = maxOverVars.get(invKey);
if (o != null) c.setCoefficient(o, -1.0);
absoluteCount++;
}
}
// === 目标库存 ===
if (spec.hasTarget()) {
if (spec.hasTargetInDays()) {
usedInDays = true;
MPConstraint c = model.getSolver().makeConstraint(
0.0, 0.0, "TargetInv_" + invKey);
c.setCoefficient(invVar, 1.0);
MPVariable u = targetUnderVars.get(invKey);
if (u != null) c.setCoefficient(u, 1.0);
LOG.info(String.format("[TargetInv_天] %s: 目标库存=%d天",
invKey, (int) spec.getTargetInDays()));
addDemandFulfillmentTerms(c, invKey, dfVars, periods, periodIdx,
spec.getTargetInDays(), "TargetInv");
} else {
double rhs = spec.getTargetLevel();
MPConstraint c = model.getSolver().makeConstraint(
rhs, rhs, "TargetInv_" + invKey);
c.setCoefficient(invVar, 1.0);
MPVariable u = targetUnderVars.get(invKey);
if (u != null) c.setCoefficient(u, 1.0);
absoluteCount++;
}
}
if (usedInDays) inDaysCount++;
}
LOG.info(String.format("[库存规格] 构建完成: %d 个使用安全库存天数, %d 个使用绝对数量",
inDaysCount, absoluteCount));
}
/**
* 向约束添加 DemandFulfillment 项 (委托给 {@link SafetyStockCalculator})。
*
* <p>添加负系数项, 效果是:
* InvQty + Slack ≥ Σ(ratio × DemandFulfillment)</p>
*
* @param constraint 库存约束
* @param invKey 库存 key (productId_spId_periodIndex)
* @param dfVars DemandFulfillment 变量映射
* @param periods 周期列表
* @param currentPeriodIdx 当前周期索引
* @param safetyStockDays 安全库存天数
* @param constraintType 约束类型 (MinInv / MaxInv / TargetInv), 仅用于日志
*/
private static void addDemandFulfillmentTerms(
MPConstraint constraint,
String invKey,
Map<String, MPVariable> dfVars,
List<Period> periods,
int currentPeriodIdx,
double safetyStockDays,
String constraintType) {
// 委托给工具类计算折算项
List<SafetyStockTerm> terms = SafetyStockCalculator.calculate(
currentPeriodIdx, safetyStockDays, periods, invKey);
if (terms.isEmpty()) {
LOG.warning(String.format("[%s] %s: 安全库存天数=%.2f天, 但没有找到未来周期!",
constraintType, invKey, safetyStockDays));
return;
}
// 将折算项添加到约束
int termsAdded = 0;
for (SafetyStockTerm term : terms) {
MPVariable dfVar = dfVars.get(term.getDemandFulfillmentKey());
if (dfVar != null) {
// 负系数: -ratio × DemandFulfillment
// 约束: InvQty + Slack ≥ 0
// 移项: InvQty + Slack ≥ Σ(ratio × DemandFulfillment)
constraint.setCoefficient(dfVar, -term.getRatio());
termsAdded++;
} else {
LOG.warning(String.format("[%s] %s: DemandFulfillment 变量 '%s' 不存在! "
+ "周期 %s 的需求预测未计入安全库存约束",
constraintType, invKey, term.getDemandFulfillmentKey(),
term.getPeriodName()));
}
}
LOG.fine(String.format("[%s] %s: 已添加 %d/%d 个 DemandFulfillment 项到约束",
constraintType, invKey, termsAdded, terms.size()));
}
}
\ No newline at end of file
package com.aps.macroplanner.constraint;
import com.google.ortools.linearsolver.MPConstraint;
import com.google.ortools.linearsolver.MPSolver;
import com.google.ortools.linearsolver.MPVariable;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
import java.util.Map;
/**
* KPI 汇总变量及定义约束构建器
*
* <p>创建全局 KPI 汇总变量, 每个 KPI = 对应松弛/惩罚变量的总和。
* 这些汇总变量被目标函数直接引用。</p>
*
* <h3>KPI 汇总一览</h3>
* <pre>
* TotalFulfillment = Σ DemandSlack (权重 100)
* TotalLotSize = Σ PTLotSizeOver + Under (权重 10)
* TotalMaxInventoryLevel = Σ MaxInvQtyOver (权重 5)
* TotalMinInventoryLevel = Σ MinInvQtyUnder (权重 5)
* TotalTargetInvLevel = Σ InvQtyUnderTarget (权重 8)
* TotalUnitCapacity = Σ CapacityOverloaded (权重 3)
* TotalSupplyTarget = Σ SupplyTargetQtyUnder (权重 8)
* TotalMinSupply = Σ MinSupplyQtyUnder (权重 5)
* TotalMaxSupply = Σ MaxSupplyQtyOver (权重 5)
* TotalSalesDemandPriority= Σ SalesDemandQty × Priority (权重 1)
* TotalPostponementPenalty= 0 (简化版) (权重 20)
* TotalProcessMaxQuantity = 0 (简化版) (权重 5)
* </pre>
*/
public class KpiAggregator {
/**
* 创建 KPI 汇总变量及其定义约束, 并注册到模型中。
*/
public static void build(MacroPlannerModel model, TestDataBuilder data) {
double inf = MPSolver.infinity();
// TotalFulfillment = Σ DemandSlack
model.setTotalFulfillment(createSumKpi(model, "TotalFulfillment",
model.getDemandSlackVars()));
// TotalLotSize = Σ PTLotSizeOver + Σ PTLotSizeUnder
MPVariable totalLotSize = model.getSolver().makeNumVar(0.0, inf, "TotalLotSize");
MPConstraint lotDef = model.getSolver().makeConstraint(0.0, 0.0, "Def_TotalLotSize");
lotDef.setCoefficient(totalLotSize, -1.0);
for (MPVariable v : model.getPtLotSizeOverVars().values()) lotDef.setCoefficient(v, 1.0);
for (MPVariable v : model.getPtLotSizeUnderVars().values()) lotDef.setCoefficient(v, 1.0);
model.setTotalLotSize(totalLotSize);
// TotalMaxInventoryLevel = Σ MaxInvQtyOver
model.setTotalMaxInventoryLevel(createSumKpi(model, "TotalMaxInvLevel",
model.getMaxInvQtyOverVars()));
// TotalMinInventoryLevel = Σ MinInvQtyUnder
model.setTotalMinInventoryLevel(createSumKpi(model, "TotalMinInvLevel",
model.getMinInvQtyUnderVars()));
// TotalTargetInvLevel = Σ InvQtyUnderTarget
model.setTotalTargetInvLevel(createSumKpi(model, "TotalTargetInvLevel",
model.getInvQtyUnderTargetVars()));
// TotalUnitCapacity = Σ CapacityOverloaded
model.setTotalUnitCapacity(createSumKpi(model, "TotalUnitCapacity",
model.getCapacityOverloadedVars()));
// TotalSupplyTarget = Σ SupplyTargetQtyUnder
model.setTotalSupplyTarget(createSumKpi(model, "TotalSupplyTarget",
model.getSupplyTargetQtyUnderVars()));
// TotalMinSupply = Σ MinSupplyQtyUnder
model.setTotalMinSupply(createSumKpi(model, "TotalMinSupply",
model.getMinSupplyQtyUnderVars()));
// TotalMaxSupply = Σ MaxSupplyQtyOver
model.setTotalMaxSupply(createSumKpi(model, "TotalMaxSupply",
model.getMaxSupplyQtyOverVars()));
// TotalSalesDemandPriority = Σ SalesDemandQty × Priority
MPVariable totalSDPri = model.getSolver().makeNumVar(0.0, inf, "TotalSalesDemandPriority");
MPConstraint sdDef = model.getSolver().makeConstraint(0.0, 0.0, "Def_TotalSDPriority");
sdDef.setCoefficient(totalSDPri, -1.0);
for (SalesDemand sd : data.getSalesDemands()) {
MPVariable sdVar = model.getSalesDemandQtyVars().get(sd.getKey());
if (sdVar != null) sdDef.setCoefficient(sdVar, sd.getPriority());
}
model.setTotalSalesDemandPriority(totalSDPri);
// 简化版 KPI (固定为 0)
model.setTotalPostponementPenalty(
model.getSolver().makeNumVar(0.0, 0.0, "TotalPostponementPenalty"));
model.setTotalProcessMaxQuantity(
model.getSolver().makeNumVar(0.0, 0.0, "TotalProcessMaxQty"));
}
/** 辅助方法: 创建 SumVar = Σ(values) 的汇总变量 */
private static MPVariable createSumKpi(MacroPlannerModel model, String name,
Map<String, MPVariable> sourceVars) {
MPVariable sumVar = model.getSolver().makeNumVar(0.0, MPSolver.infinity(), name);
MPConstraint def = model.getSolver().makeConstraint(0.0, 0.0, "Def_" + name);
def.setCoefficient(sumVar, -1.0);
for (MPVariable v : sourceVars.values()) {
if (v != null) def.setCoefficient(v, 1.0);
}
return sumVar;
}
}
\ No newline at end of file
package com.aps.macroplanner.constraint;
import com.google.ortools.linearsolver.MPConstraint;
import com.google.ortools.linearsolver.MPSolver;
import com.google.ortools.linearsolver.MPVariable;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
import java.util.Map;
/**
* 批次大小约束构建器 (LotSizeConstraint)
*
* <p>确保每个操作的生产量符合批次大小要求。</p>
*
* <h3>数学公式 (简化版, PTNrOfLots=1)</h3>
* <pre>
* 批次下限: PTQty × QTPFactor + PTLotSizeOver ≥ LotSize
* 批次上限: PTQty × QTPFactor - PTLotSizeUnder ≤ LotSize
* </pre>
*/
public class LotSizeConstraint {
/**
* 构建批次大小约束。
*/
public static void build(MacroPlannerModel model, TestDataBuilder data) {
Map<String, MPVariable> ptQtyVars = model.getPtQtyVars();
Map<String, MPVariable> overVars = model.getPtLotSizeOverVars();
Map<String, MPVariable> underVars = model.getPtLotSizeUnderVars();
for (Operation op : data.getOperations()) {
for (UnitOperation uo : op.getUnitOperations()) {
if (!uo.hasLotSize()) continue;
for (Period p : data.getPeriods()) {
String key = op.ptQtyKey(uo, p.getIndex());
MPVariable ptVar = ptQtyVars.get(key);
if (ptVar == null) continue;
double lotSize = uo.getLotSize();
// 批次下限
MPVariable ov = overVars.get(key);
if (ov != null) {
MPConstraint c = model.getSolver().makeConstraint(
lotSize, MPSolver.infinity(), "LotOver_" + key);
c.setCoefficient(ptVar, uo.getQtpfactor());
c.setCoefficient(ov, 1.0);
}
// 批次上限
MPVariable uv = underVars.get(key);
if (uv != null) {
MPConstraint c = model.getSolver().makeConstraint(
-MPSolver.infinity(), lotSize, "LotUnder_" + key);
c.setCoefficient(ptVar, uo.getQtpfactor());
c.setCoefficient(uv, -1.0);
}
}
}
}
}
}
\ No newline at end of file
package com.aps.macroplanner.constraint;
import com.google.ortools.linearsolver.MPConstraint;
import com.google.ortools.linearsolver.MPSolver;
import com.google.ortools.linearsolver.MPVariable;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
import java.util.Map;
/**
* 供应规格约束构建器 (SupplySpecConstraint)
*
* <p>跨周期汇总某个供应规格的总供应量, 与目标/最小/最大值比较。
* 不同于库存规格 (按周期), 供应规格是跨所有周期的总量约束。</p>
*
* <h3>数学公式</h3>
* <pre>
* 目标供应: ΣPTQty + SupplyTargetQtyUnder ≥ TargetQty
* 最小供应: ΣPTQty + MinSupplyQtyUnder ≥ MinQty
* 最大供应: ΣPTQty - MaxSupplyQtyOver ≤ MaxQty
* </pre>
*/
public class SupplySpecConstraint {
/**
* 构建供应规格约束。
*/
public static void build(MacroPlannerModel model, TestDataBuilder data) {
Map<String, MPVariable> ptQtyVars = model.getPtQtyVars();
Map<String, MPVariable> targetUnderVars = model.getSupplyTargetQtyUnderVars();
Map<String, MPVariable> minUnderVars = model.getMinSupplyQtyUnderVars();
Map<String, MPVariable> maxOverVars = model.getMaxSupplyQtyOverVars();
for (SupplySpec spec : data.getSupplySpecs()) {
// 目标供应
MPConstraint target = model.getSolver().makeConstraint(
spec.getTargetQuantity(), MPSolver.infinity(),
"TargetSupply_" + spec.getName());
addPtQtyTerms(ptQtyVars, spec, data, target);
MPVariable tu = targetUnderVars.get(spec.getName());
if (tu != null) target.setCoefficient(tu, 1.0);
// 最小供应
MPConstraint min = model.getSolver().makeConstraint(
spec.getMinQuantity(), MPSolver.infinity(),
"MinSupply_" + spec.getName());
addPtQtyTerms(ptQtyVars, spec, data, min);
MPVariable mu = minUnderVars.get(spec.getName());
if (mu != null) min.setCoefficient(mu, 1.0);
// 最大供应
if (spec.hasMaxQuantity()) {
MPConstraint max = model.getSolver().makeConstraint(
-MPSolver.infinity(), spec.getMaxQuantity(),
"MaxSupply_" + spec.getName());
addPtQtyTerms(ptQtyVars, spec, data, max);
MPVariable mo = maxOverVars.get(spec.getName());
if (mo != null) max.setCoefficient(mo, -1.0);
}
}
}
/** 将供应规格关联的所有操作(所有Unit)、所有周期的 PTQty 变量加入约束 */
private static void addPtQtyTerms(Map<String, MPVariable> ptQtyVars, SupplySpec spec,
TestDataBuilder data, MPConstraint constraint) {
for (Operation op : spec.getOperations()) {
for (UnitOperation uo : op.getUnitOperations()) {
for (Period p : data.getPeriods()) {
MPVariable ptVar = ptQtyVars.get(op.ptQtyKey(uo, p.getIndex()));
if (ptVar != null) constraint.setCoefficient(ptVar, 1.0);
}
}
}
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
/**
* 库存规格 (InventorySpec) — 对应 Quintiq 中的 InventorySpecification
*
* <p>定义某个产品/库存点/周期的库存水平约束。
* 支持两种模式:</p>
* <ul>
* <li><b>绝对数量模式</b>: targetLevel/minLevel/maxLevel 直接指定库存数量</li>
* <li><b>安全库存天数模式</b>: targetInDays/minLevelInDays/maxLevelInDays 指定天数,
* 约束的 RHS 由未来周期的 DemandFulfillment 动态计算</li>
* </ul>
*/
public class InventorySpec {
private final Product product;
private final StockingPoint stockingPoint;
private final Period period;
// === 绝对数量模式 ===
private final double targetLevel;
private final double minLevel;
private final double maxLevel;
private final boolean hasTarget;
private final boolean hasMinLevel;
private final boolean hasMaxLevel;
// === 安全库存天数模式 ===
/** 目标库存天数 (如 5 天), 0 表示不使用天数模式 */
private final double targetInDays;
/** 最小库存天数 (如 3 天) */
private final double minLevelInDays;
/** 最大库存天数 (如 10 天) */
private final double maxLevelInDays;
private final boolean hasTargetInDays;
private final boolean hasMinLevelInDays;
private final boolean hasMaxLevelInDays;
public InventorySpec(Product product, StockingPoint stockingPoint, Period period,
double targetLevel, double minLevel, double maxLevel,
boolean hasTarget, boolean hasMinLevel, boolean hasMaxLevel) {
this(product, stockingPoint, period,
targetLevel, minLevel, maxLevel, hasTarget, hasMinLevel, hasMaxLevel,
0.0, 0.0, 0.0, false, false, false);
}
public InventorySpec(Product product, StockingPoint stockingPoint, Period period,
double targetLevel, double minLevel, double maxLevel,
boolean hasTarget, boolean hasMinLevel, boolean hasMaxLevel,
double targetInDays, double minLevelInDays, double maxLevelInDays,
boolean hasTargetInDays, boolean hasMinLevelInDays, boolean hasMaxLevelInDays) {
this.product = product;
this.stockingPoint = stockingPoint;
this.period = period;
this.targetLevel = targetLevel;
this.minLevel = minLevel;
this.maxLevel = maxLevel;
this.hasTarget = hasTarget;
this.hasMinLevel = hasMinLevel;
this.hasMaxLevel = hasMaxLevel;
this.targetInDays = targetInDays;
this.minLevelInDays = minLevelInDays;
this.maxLevelInDays = maxLevelInDays;
this.hasTargetInDays = hasTargetInDays;
this.hasMinLevelInDays = hasMinLevelInDays;
this.hasMaxLevelInDays = hasMaxLevelInDays;
}
public Product getProduct() { return product; }
public StockingPoint getStockingPoint() { return stockingPoint; }
public Period getPeriod() { return period; }
public double getTargetLevel() { return targetLevel; }
public double getMinLevel() { return minLevel; }
public double getMaxLevel() { return maxLevel; }
public boolean hasTarget() { return hasTarget; }
public boolean hasMinLevel() { return hasMinLevel; }
public boolean hasMaxLevel() { return hasMaxLevel; }
public double getTargetInDays() { return targetInDays; }
public double getMinLevelInDays() { return minLevelInDays; }
public double getMaxLevelInDays() { return maxLevelInDays; }
public boolean hasTargetInDays() { return hasTargetInDays; }
public boolean hasMinLevelInDays() { return hasMinLevelInDays; }
public boolean hasMaxLevelInDays() { return hasMaxLevelInDays; }
public String getKey() {
return product.getId() + "_" + stockingPoint.getId() + "_" + period.getIndex();
}
@Override
public String toString() {
return "InvSpec[" + product + ", " + stockingPoint + ", " + period + "]";
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
/**
* KPI 权重配置 — 对应 Quintiq 中的 KPIWeightMacroPlan
* 定义每个 KPI 的优化层级和权重
*/
public class KPIWeights {
// 非财务 KPI 权重
private final double fulfillmentWeight;
private final double lotSizeWeight;
private final double maxInventoryLevelWeight;
private final double minInventoryLevelWeight;
private final double targetInventoryLevelWeight;
private final double unitCapacityWeight;
private final double supplyTargetWeight;
private final double minSupplyWeight;
private final double maxSupplyWeight;
private final double salesDemandPriorityWeight;
private final double postponementPenaltyWeight;
private final double processMaxQuantityWeight;
public KPIWeights(double fulfillmentWeight, double lotSizeWeight,
double maxInventoryLevelWeight, double minInventoryLevelWeight,
double targetInventoryLevelWeight, double unitCapacityWeight,
double supplyTargetWeight, double minSupplyWeight,
double maxSupplyWeight, double salesDemandPriorityWeight,
double postponementPenaltyWeight, double processMaxQuantityWeight) {
this.fulfillmentWeight = fulfillmentWeight;
this.lotSizeWeight = lotSizeWeight;
this.maxInventoryLevelWeight = maxInventoryLevelWeight;
this.minInventoryLevelWeight = minInventoryLevelWeight;
this.targetInventoryLevelWeight = targetInventoryLevelWeight;
this.unitCapacityWeight = unitCapacityWeight;
this.supplyTargetWeight = supplyTargetWeight;
this.minSupplyWeight = minSupplyWeight;
this.maxSupplyWeight = maxSupplyWeight;
this.salesDemandPriorityWeight = salesDemandPriorityWeight;
this.postponementPenaltyWeight = postponementPenaltyWeight;
this.processMaxQuantityWeight = processMaxQuantityWeight;
}
public double getFulfillmentWeight() { return fulfillmentWeight; }
public double getLotSizeWeight() { return lotSizeWeight; }
public double getMaxInventoryLevelWeight() { return maxInventoryLevelWeight; }
public double getMinInventoryLevelWeight() { return minInventoryLevelWeight; }
public double getTargetInventoryLevelWeight() { return targetInventoryLevelWeight; }
public double getUnitCapacityWeight() { return unitCapacityWeight; }
public double getSupplyTargetWeight() { return supplyTargetWeight; }
public double getMinSupplyWeight() { return minSupplyWeight; }
public double getMaxSupplyWeight() { return maxSupplyWeight; }
public double getSalesDemandPriorityWeight() { return salesDemandPriorityWeight; }
public double getPostponementPenaltyWeight() { return postponementPenaltyWeight; }
public double getProcessMaxQuantityWeight() { return processMaxQuantityWeight; }
}
\ No newline at end of file
......@@ -165,18 +165,24 @@ public class MacroPlannerDataConverter {
.eq(ProdLaunchOrder::getSceneId, sceneId));
log.info("加载订单: {} 条", ctx.prodLaunchOrders.size());
// 2. 收集 routingIds
List<Integer> routingIds = ctx.prodLaunchOrders.stream()
.map(ProdLaunchOrder::getRoutingId)
// 2. 收集 materialIds
Set<String> materialIds = ctx.prodLaunchOrders.stream()
.map(ProdLaunchOrder::getMaterialId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
.collect(Collectors.toSet());
List<Integer> routingIds=null;
// 3. 工艺路线头表
if (!routingIds.isEmpty()) {
if (!materialIds.isEmpty()) {
ctx.routingHeaders = routingHeaderMapper.selectList(
new LambdaQueryWrapper<RoutingHeader>()
.in(RoutingHeader::getId, routingIds));
.in(RoutingHeader::getMaterialId, materialIds));
routingIds = ctx.routingHeaders.stream()
.map(RoutingHeader::getId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
}
log.info("加载工艺路线: {} 条", ctx.routingHeaders.size());
......@@ -202,16 +208,12 @@ public class MacroPlannerDataConverter {
log.info("加载工艺物料消耗: {} 条", ctx.routingsupportings.size());
// 6. 收集所有 materialId (订单 + 工艺物料消耗 + 工艺路线头表)
Set<String> materialIds = new HashSet<>();
ctx.prodLaunchOrders.forEach(o -> {
if (o.getMaterialId() != null) materialIds.add(o.getMaterialId());
});
ctx.routingsupportings.forEach(rs -> {
if (rs.getMaterialId() != null) materialIds.add(rs.getMaterialId());
});
ctx.routingHeaders.forEach(rh -> {
if (rh.getMaterialId() != null) materialIds.add(rh.getMaterialId());
});
// 7. 物料主数据
if (!materialIds.isEmpty()) {
......
package com.aps.macroplanner.data;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* 操作/工艺 (Operation) — 对应 Quintiq 中的 Operation。
* 表示一个生产操作,可通过多个 {@link UnitOperation} 在不同单元上执行,
* 每个单元可有不同的产能消耗系数和批次参数。
*
* <h3>多产出支持 (联产品 / 副产品)</h3>
* 一个 Operation 可以有多个 {@link OperationOutput},对应 Quintiq 中
* Operation 可以产出到多个 PISP (Product in Stocking Point)。
* 例如: 炼油工序同时产出汽油和柴油。
*
* <h3>向后兼容</h3>
* {@link #getOutput()} / {@link #getOutputProductId()} / {@link #getOutputSpId()}
* 返回第一个产出,保持与旧代码的兼容。
* 新代码应使用 {@link #getOutputs()} 并遍历所有产出。
*/
public class Operation {
private final String id;
private final String name;
private final List<UnitOperation> unitOperations; // 可在多个单元执行, 每个单元参数不同
private final List<OperationOutput> outputs; // 产出列表 (产品 + 库存点), 对应 Quintiq 中 OperationOutput→PISP
private final double relativeDuration; // 产出系数 (每 PTQty 单位产出多少件)
private final int leadTimeDays; // 提前期(天数): 生产/采购当天, 入库在 productionDate + leadTimeDays 天
// ==================== 新构造器 (推荐使用) ====================
/** 完整构造器 (有产出 + leadTime) */
public Operation(String id, String name, List<UnitOperation> unitOperations,
OperationOutput output, double relativeDuration, int leadTimeDays) {
this.id = id;
this.name = name;
this.unitOperations = new ArrayList<>(unitOperations);
this.outputs = new ArrayList<>();
if (output != null) this.outputs.add(output);
this.relativeDuration = relativeDuration;
this.leadTimeDays = leadTimeDays;
}
/** 有产出, leadTime=0 */
public Operation(String id, String name, List<UnitOperation> unitOperations,
OperationOutput output, double relativeDuration) {
this(id, name, unitOperations, output, relativeDuration, 0);
}
/** 无产出 (Routing 用), 有 leadTime */
public Operation(String id, String name, List<UnitOperation> unitOperations,
double relativeDuration, int leadTimeDays) {
this(id, name, unitOperations, null, relativeDuration, leadTimeDays);
}
/** 无产出, leadTime=0 */
public Operation(String id, String name, List<UnitOperation> unitOperations,
double relativeDuration) {
this(id, name, unitOperations, null, relativeDuration, 0);
}
// ==================== 旧构造器 (保留向后兼容, 标记为 Deprecated) ====================
/** @deprecated 请使用 {@link #Operation(String, String, List, OperationOutput, double, int)} */
@Deprecated
public Operation(String id, String name, String unitId, OperationOutput output,
double capacityCoeff, double relativeDuration,
boolean hasLotSize, double lotSize, double qtpfactor,
int leadTimeDays) {
this(id, name,
Arrays.asList(new UnitOperation(unitId, capacityCoeff, hasLotSize, lotSize, qtpfactor)),
output, relativeDuration, leadTimeDays);
}
/** @deprecated 请使用 {@link #Operation(String, String, List, OperationOutput, double)} */
@Deprecated
public Operation(String id, String name, String unitId, OperationOutput output,
double capacityCoeff, double relativeDuration,
boolean hasLotSize, double lotSize, double qtpfactor) {
this(id, name, unitId, output, capacityCoeff, relativeDuration,
hasLotSize, lotSize, qtpfactor, 0);
}
/** @deprecated 请使用 {@link #Operation(String, String, List, double, int)} */
@Deprecated
public Operation(String id, String name, String unitId,
double capacityCoeff, double relativeDuration,
boolean hasLotSize, double lotSize, double qtpfactor,
int leadTimeDays) {
this(id, name, unitId, null, capacityCoeff, relativeDuration,
hasLotSize, lotSize, qtpfactor, leadTimeDays);
}
/** @deprecated 请使用 {@link #Operation(String, String, List, double)} */
@Deprecated
public Operation(String id, String name, String unitId,
double capacityCoeff, double relativeDuration,
boolean hasLotSize, double lotSize, double qtpfactor) {
this(id, name, unitId, null, capacityCoeff, relativeDuration,
hasLotSize, lotSize, qtpfactor, 0);
}
// ==================== Getters ====================
public String getId() { return id; }
public String getName() { return name; }
/** 获取所有 UnitOperation (推荐使用)。 */
public List<UnitOperation> getUnitOperations() {
return Collections.unmodifiableList(unitOperations);
}
/**
* 生成 PTQty 变量 key: {operationId}_{unitId}_{periodIndex}。
* 统一 key 格式, 避免在多个文件中硬编码拼接。
*/
public String ptQtyKey(UnitOperation uo, int periodIndex) {
return id + "_" + uo.getUnitId() + "_" + periodIndex;
}
// === 向后兼容的旧 getter (委托给第一个 UnitOperation) ===
/** @deprecated 请使用 {@link #getUnitOperations()}.get(0).getUnitId() */
@Deprecated
public String getUnitId() { return unitOperations.isEmpty() ? null : unitOperations.get(0).getUnitId(); }
/** @deprecated 请使用 {@link #getUnitOperations()}.get(0).getCapacityCoeff() */
@Deprecated
public double getCapacityCoeff() { return unitOperations.isEmpty() ? 0 : unitOperations.get(0).getCapacityCoeff(); }
/** @deprecated 请使用 {@link #getUnitOperations()}.get(0).hasLotSize() */
@Deprecated
public boolean hasLotSize() { return !unitOperations.isEmpty() && unitOperations.get(0).hasLotSize(); }
/** @deprecated 请使用 {@link #getUnitOperations()}.get(0).getLotSize() */
@Deprecated
public double getLotSize() { return unitOperations.isEmpty() ? 0 : unitOperations.get(0).getLotSize(); }
/** @deprecated 请使用 {@link #getUnitOperations()}.get(0).getQtpfactor() */
@Deprecated
public double getQtpfactor() { return unitOperations.isEmpty() ? 0 : unitOperations.get(0).getQtpfactor(); }
/**
* 获取所有产出列表。
* 对应 Quintiq 中 Operation 的多个 OperationOutput→PISP 关联。
*/
public List<OperationOutput> getOutputs() {
return Collections.unmodifiableList(outputs);
}
/** 添加产出 (联产品/副产品)。 */
public void addOutput(OperationOutput output) {
if (output != null) this.outputs.add(output);
}
/**
* 设置唯一产出 (清除现有产出后添加)。
* 由 {@link Routing#expand} 调用,自动配置中间 WIP 库存点。
*/
public void setOutput(OperationOutput output) {
this.outputs.clear();
if (output != null) this.outputs.add(output);
}
// === 向后兼容的便捷方法 (委托给第一个产出) ===
/**
* 获取第一个产出。
* @deprecated 新代码应使用 {@link #getOutputs()} 遍历所有产出。
*/
public OperationOutput getOutput() {
return outputs.isEmpty() ? null : outputs.get(0);
}
/**
* 获取第一个产出的产品 ID。
* @deprecated 新代码应使用 {@link #getOutputs()} 遍历所有产出。
*/
public String getOutputProductId() {
return outputs.isEmpty() ? null : outputs.get(0).getProductId();
}
/**
* 获取第一个产出的库存点 ID。
* @deprecated 新代码应使用 {@link #getOutputs()} 遍历所有产出。
*/
public String getOutputSpId() {
return outputs.isEmpty() ? null : outputs.get(0).getSpId();
}
// ==================== 多产出查询方法 ====================
/**
* 检查该工序是否产出指定产品 (任一库存点)。
*/
public boolean producesProduct(String productId) {
for (OperationOutput oo : outputs) {
if (oo.getProductId().equals(productId)) return true;
}
return false;
}
/**
* 检查该工序是否产出指定产品到指定库存点。
*/
public boolean producesProductAtSp(String productId, String spId) {
for (OperationOutput oo : outputs) {
if (oo.getProductId().equals(productId) && oo.getSpId().equals(spId)) return true;
}
return false;
}
/**
* 获取所有产出到指定产品+库存点的产出对象。
*/
public List<OperationOutput> getOutputsForProductAtSp(String productId, String spId) {
List<OperationOutput> result = new ArrayList<>();
for (OperationOutput oo : outputs) {
if (oo.getProductId().equals(productId) && oo.getSpId().equals(spId)) {
result.add(oo);
}
}
return result;
}
public double getRelativeDuration() { return relativeDuration; }
public int getLeadTimeDays() { return leadTimeDays; }
@Override
public String toString() {
if (outputs.isEmpty()) return name + "(" + id + ")";
if (outputs.size() == 1) return name + "(" + id + ") → " + outputs.get(0);
return name + "(" + id + ") → " + outputs;
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
/**
* 操作输入物料 (OperationInput) — 对应 Quintiq 中的 OperationInput
*
* 定义某个操作 (Operation) 生产时需要消耗的原材料/半成品,
* 即 BOM (Bill of Materials) 中的输入物料关系。
*
* 例如: 生产 1 个 ProductA 需要消耗 0.5 个 ProductB
*/
public class OperationInput {
private final Operation operation; // 所属操作
private final Product inputProduct; // 输入物料产品
private final StockingPoint inputSp; // 输入物料来源库存点
private final double factor; // 消耗因子 (每单位 PTQty 消耗的输入物料量)
public OperationInput(Operation operation, Product inputProduct,
StockingPoint inputSp, double factor) {
this.operation = operation;
this.inputProduct = inputProduct;
this.inputSp = inputSp;
this.factor = factor;
}
public Operation getOperation() { return operation; }
public Product getInputProduct() { return inputProduct; }
public StockingPoint getInputSp() { return inputSp; }
public double getFactor() { return factor; }
public String getKey() {
return operation.getId() + "_" + inputProduct.getId() + "_" + inputSp.getId();
}
@Override
public String toString() {
return "OpInput[" + operation.getName() + " <- " + inputProduct.getName()
+ "@" + inputSp.getName() + " x" + factor + "]";
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
import java.time.LocalDate;
/**
* 周期 (Period) — 对应 Quintiq 中的 Period_MP
*
* <p>startDate 用于 leadTime 日期偏移计算:
* 生产在周期 t, 入库在 t+leadTimeDays 天, 通过 startDate 精确定位到目标周期。</p>
*
* <p>durationInDays 用于安全库存天数计算:
* 当库存规格以"天"为单位时, 需要知道每个周期的长度来折算比例。</p>
*/
public class Period {
private final int index;
private final String name;
/** 周期长度 (天), 默认 1.0。用于安全库存天数折算。 */
private final double durationInDays;
/** 周期起始日期, 用于 leadTime 日期偏移计算。null 表示未指定(兼容旧数据)。 */
private final LocalDate startDate;
public Period(int index, String name) {
this(index, name, 1.0, null);
}
public Period(int index, String name, double durationInDays) {
this(index, name, durationInDays, null);
}
public Period(int index, String name, LocalDate startDate) {
this(index, name, 1.0, startDate);
}
public Period(int index, String name, double durationInDays, LocalDate startDate) {
this.index = index;
this.name = name;
this.durationInDays = durationInDays;
this.startDate = startDate;
}
public int getIndex() { return index; }
public String getName() { return name; }
public double getDurationInDays() { return durationInDays; }
public LocalDate getStartDate() { return startDate; }
/**
* 判断给定日期是否落在本周期内。
* 周期区间: [startDate, nextPeriod.startDate) 或 [startDate, startDate + durationInDays)
*/
public boolean contains(LocalDate date) {
if (startDate == null || date == null) return false;
LocalDate end = startDate.plusDays((long) durationInDays);
return !date.isBefore(startDate) && date.isBefore(end);
}
@Override
public String toString() {
return name;
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
/**
* 产品 (Product) — 对应 Quintiq 中的 Product_MP
*/
public class Product {
private final String id;
private final String name;
public Product(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() { return id; }
public String getName() { return name; }
@Override
public String toString() {
return name + "1(" + id + ")";
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
/**
* 销售需求 (SalesDemand) — 对应 Quintiq 中的 SalesDemand
* 表示某个产品在某个库存点、某个周期的销售需求
*/
public class SalesDemand {
private final Product product;
private final StockingPoint stockingPoint;
private final Period period;
private final double quantity; // 需求总量
private final double priority; // 需求优先级
public SalesDemand(Product product, StockingPoint stockingPoint, Period period,
double quantity, double priority) {
this.product = product;
this.stockingPoint = stockingPoint;
this.period = period;
this.quantity = quantity;
this.priority = priority;
}
public Product getProduct() { return product; }
public StockingPoint getStockingPoint() { return stockingPoint; }
public Period getPeriod() { return period; }
public double getQuantity() { return quantity; }
public double getPriority() { return priority; }
public String getKey() {
return product.getId() + "_" + stockingPoint.getId() + "_" + period.getIndex();
}
@Override
public String toString() {
return "SD[" + product + ", " + stockingPoint + ", " + period + "] qty=" + quantity;
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
/**
* 库存点 (StockingPoint) — 对应 Quintiq 中的 StockingPoint_MP
*/
public class StockingPoint {
private final String id;
private final String name;
public StockingPoint(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() { return id; }
public String getName() { return name; }
@Override
public String toString() {
return name + "(" + id + ")";
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
import java.util.List;
/**
* 供应规格 (SupplySpec) — 对应 Quintiq 中的 SupplySpecification
* 定义某个供应规格的目标/最小/最大供应量
*/
public class SupplySpec {
private final String name;
private final double targetQuantity;
private final double minQuantity;
private final double maxQuantity;
private final boolean hasMaxQuantity;
private final List<Operation> operations; // 该规格关联的操作
public SupplySpec(String name, double targetQuantity, double minQuantity,
double maxQuantity, boolean hasMaxQuantity, List<Operation> operations) {
this.name = name;
this.targetQuantity = targetQuantity;
this.minQuantity = minQuantity;
this.maxQuantity = maxQuantity;
this.hasMaxQuantity = hasMaxQuantity;
this.operations = operations;
}
public String getName() { return name; }
public double getTargetQuantity() { return targetQuantity; }
public double getMinQuantity() { return minQuantity; }
public double getMaxQuantity() { return maxQuantity; }
public boolean hasMaxQuantity() { return hasMaxQuantity; }
public List<Operation> getOperations() { return operations; }
@Override
public String toString() {
return "SupplySpec[" + name + "] target=" + targetQuantity + " min=" + minQuantity + " max=" + maxQuantity;
}
}
\ No newline at end of file
This diff is collapsed.
package com.aps.macroplanner.data;
/**
* 单元周期 (UnitPeriod) — 对应 Quintiq 中的 UnitPeriod
* 表示某个单元在某个周期的产能可用情况
*/
public class UnitPeriod {
private final String unitId;
private final Period period;
private final double minCapacity; // 最小产能
private final double maxCapacity; // 最大可用产能
private final boolean hasMinCapacity; // 是否有最小产能约束
public UnitPeriod(String unitId, Period period, double minCapacity, double maxCapacity, boolean hasMinCapacity) {
this.unitId = unitId;
this.period = period;
this.minCapacity = minCapacity;
this.maxCapacity = maxCapacity;
this.hasMinCapacity = hasMinCapacity;
}
public String getUnitId() { return unitId; }
public Period getPeriod() { return period; }
public double getMinCapacity() { return minCapacity; }
public double getMaxCapacity() { return maxCapacity; }
public boolean hasMinCapacity() { return hasMinCapacity; }
public String getKey() {
return unitId + "_" + period.getIndex();
}
@Override
public String toString() {
return "UnitPeriod[" + unitId + ", " + period + "]";
}
}
\ No newline at end of file
package com.aps.macroplanner.model;
import com.google.ortools.linearsolver.MPSolver;
import com.google.ortools.linearsolver.MPVariable;
import java.util.HashMap;
import java.util.Map;
/**
* 优化模型容器 — 持有求解器、所有决策变量和 KPI 汇总变量。
*
* <p>设计原则: 所有变量通过此类的 getter 方法暴露,
* 各约束构建器和目标函数构建器通过此模型相互协作。</p>
*
* <h3>变量索引键命名规则</h3>
* <pre>
* 生产变量: {operationId}_{periodIndex}
* 库存变量: {productId}_{spId}_{periodIndex}
* 准能松弛: {unitId}_{periodIndex}
* 供应松弛: {specName}
* 批次松弛: {operationId}_{periodIndex}
* BOM 变量: {operationId}_{productId}_{spId}_{periodIndex}
* </pre>
*/
public class MacroPlannerModel {
// ==================== 求解器 ====================
/** MIP 求解器实例 (SCIP) */
private final MPSolver solver;
// ==================== 生产变量 ====================
/** PTQty[operationId_periodIndex] — 生产量, 范围 [0, +∞) */
private final Map<String, MPVariable> ptQtyVars = new HashMap<>();
// ==================== 库存变量 ====================
/** InvQty[productId_spId_periodIndex] — 期末库存, 范围 [0, +∞) */
private final Map<String, MPVariable> invQtyVars = new HashMap<>();
// ==================== 需求变量 ====================
/** SalesDemandQty[productId_spId_periodIndex] — 销售需求满足量, 范围 [0, 需求量] */
private final Map<String, MPVariable> salesDemandQtyVars = new HashMap<>();
/** DemandSlack[productId_spId_periodIndex] — 需求松弛, 范围 [0, +∞) */
private final Map<String, MPVariable> demandSlackVars = new HashMap<>();
/** OperationDemandQty[inputKey_periodIndex] — BOM 消耗量, 范围 [0, +∞) */
private final Map<String, MPVariable> operationDemandQtyVars = new HashMap<>();
/** DependentDemandInPISPIP[productId_spId_periodIndex] — PISPIP 总依赖需求, 范围 [0, +∞) */
private final Map<String, MPVariable> dependentDemandVars = new HashMap<>();
/** DemandFulfillmentInPISPIP[productId_spId_periodIndex] — PISPIP 总需求满足量,
* 用于安全库存天数计算。等于 SalesDemandQty + OperationDemandQty 之和。
* 范围 [0, +∞) */
private final Map<String, MPVariable> demandFulfillmentVars = new HashMap<>();
// ==================== 产能松弛变量 ====================
/** CapacityOverloaded[unitId_periodIndex] — 产能超载, 范围 [0, +∞) */
private final Map<String, MPVariable> capacityOverloadedVars = new HashMap<>();
/** CapacityNotMet[unitId_periodIndex] — 产能未满足, 范围 [0, +∞) */
private final Map<String, MPVariable> capacityNotMetVars = new HashMap<>();
// ==================== 库存松弛变量 ====================
/** MinInvQtyUnder[productId_spId_periodIndex] — 低于最小库存, 范围 [0, +∞) */
private final Map<String, MPVariable> minInvQtyUnderVars = new HashMap<>();
/** MaxInvQtyOver[productId_spId_periodIndex] — 超过最大库存, 范围 [0, +∞) */
private final Map<String, MPVariable> maxInvQtyOverVars = new HashMap<>();
/** InvQtyUnderTarget[productId_spId_periodIndex] — 低于目标库存, 范围 [0, +∞) */
private final Map<String, MPVariable> invQtyUnderTargetVars = new HashMap<>();
// ==================== 供应松弛变量 ====================
/** SupplyTargetQtyUnder[specName] — 供应目标不足, 范围 [0, +∞) */
private final Map<String, MPVariable> supplyTargetQtyUnderVars = new HashMap<>();
/** MinSupplyQtyUnder[specName] — 最小供应不足, 范围 [0, +∞) */
private final Map<String, MPVariable> minSupplyQtyUnderVars = new HashMap<>();
/** MaxSupplyQtyOver[specName] — 最大供应超出, 范围 [0, +∞) */
private final Map<String, MPVariable> maxSupplyQtyOverVars = new HashMap<>();
// ==================== 批次松弛变量 ====================
/** PTLotSizeOver[operationId_periodIndex] — 批次超出, 范围 [0, +∞) */
private final Map<String, MPVariable> ptLotSizeOverVars = new HashMap<>();
/** PTLotSizeUnder[operationId_periodIndex] — 批次不足, 范围 [0, +∞) */
private final Map<String, MPVariable> ptLotSizeUnderVars = new HashMap<>();
// ==================== KPI 汇总变量 ====================
private MPVariable totalFulfillment;
private MPVariable totalLotSize;
private MPVariable totalMaxInventoryLevel;
private MPVariable totalMinInventoryLevel;
private MPVariable totalTargetInvLevel;
private MPVariable totalUnitCapacity;
private MPVariable totalSupplyTarget;
private MPVariable totalMinSupply;
private MPVariable totalMaxSupply;
private MPVariable totalSalesDemandPriority;
private MPVariable totalPostponementPenalty;
private MPVariable totalProcessMaxQuantity;
// ==================== 构造 ====================
/**
* 创建模型容器, 初始化 SCIP 求解器。
* 变量创建由 {@link VariableFactory} 负责。
*/
public MacroPlannerModel() {
this.solver = MPSolver.createSolver("SCIP");
if (solver == null) {
throw new RuntimeException("无法加载 SCIP 求解器,请检查 OR-Tools 依赖");
}
solver.enableOutput();
}
// ==================== KPI 变量 setter (由 KpiAggregator 调用) ====================
public void setTotalFulfillment(MPVariable v) { this.totalFulfillment = v; }
public void setTotalLotSize(MPVariable v) { this.totalLotSize = v; }
public void setTotalMaxInventoryLevel(MPVariable v) { this.totalMaxInventoryLevel = v; }
public void setTotalMinInventoryLevel(MPVariable v) { this.totalMinInventoryLevel = v; }
public void setTotalTargetInvLevel(MPVariable v) { this.totalTargetInvLevel = v; }
public void setTotalUnitCapacity(MPVariable v) { this.totalUnitCapacity = v; }
public void setTotalSupplyTarget(MPVariable v) { this.totalSupplyTarget = v; }
public void setTotalMinSupply(MPVariable v) { this.totalMinSupply = v; }
public void setTotalMaxSupply(MPVariable v) { this.totalMaxSupply = v; }
public void setTotalSalesDemandPriority(MPVariable v) { this.totalSalesDemandPriority = v; }
public void setTotalPostponementPenalty(MPVariable v) { this.totalPostponementPenalty = v; }
public void setTotalProcessMaxQuantity(MPVariable v) { this.totalProcessMaxQuantity = v; }
// ==================== Getters ====================
public MPSolver getSolver() { return solver; }
public Map<String, MPVariable> getPtQtyVars() { return ptQtyVars; }
public Map<String, MPVariable> getInvQtyVars() { return invQtyVars; }
public Map<String, MPVariable> getSalesDemandQtyVars() { return salesDemandQtyVars; }
public Map<String, MPVariable> getDemandSlackVars() { return demandSlackVars; }
public Map<String, MPVariable> getOperationDemandQtyVars() { return operationDemandQtyVars; }
public Map<String, MPVariable> getDependentDemandVars() { return dependentDemandVars; }
public Map<String, MPVariable> getDemandFulfillmentVars() { return demandFulfillmentVars; }
public Map<String, MPVariable> getCapacityOverloadedVars() { return capacityOverloadedVars; }
public Map<String, MPVariable> getCapacityNotMetVars() { return capacityNotMetVars; }
public Map<String, MPVariable> getMinInvQtyUnderVars() { return minInvQtyUnderVars; }
public Map<String, MPVariable> getMaxInvQtyOverVars() { return maxInvQtyOverVars; }
public Map<String, MPVariable> getInvQtyUnderTargetVars() { return invQtyUnderTargetVars; }
public Map<String, MPVariable> getSupplyTargetQtyUnderVars() { return supplyTargetQtyUnderVars; }
public Map<String, MPVariable> getMinSupplyQtyUnderVars() { return minSupplyQtyUnderVars; }
public Map<String, MPVariable> getMaxSupplyQtyOverVars() { return maxSupplyQtyOverVars; }
public Map<String, MPVariable> getPtLotSizeOverVars() { return ptLotSizeOverVars; }
public Map<String, MPVariable> getPtLotSizeUnderVars() { return ptLotSizeUnderVars; }
public MPVariable getTotalFulfillment() { return totalFulfillment; }
public MPVariable getTotalLotSize() { return totalLotSize; }
public MPVariable getTotalMaxInventoryLevel() { return totalMaxInventoryLevel; }
public MPVariable getTotalMinInventoryLevel() { return totalMinInventoryLevel; }
public MPVariable getTotalTargetInvLevel() { return totalTargetInvLevel; }
public MPVariable getTotalUnitCapacity() { return totalUnitCapacity; }
public MPVariable getTotalSupplyTarget() { return totalSupplyTarget; }
public MPVariable getTotalMinSupply() { return totalMinSupply; }
public MPVariable getTotalMaxSupply() { return totalMaxSupply; }
public MPVariable getTotalSalesDemandPriority() { return totalSalesDemandPriority; }
public MPVariable getTotalPostponementPenalty() { return totalPostponementPenalty; }
public MPVariable getTotalProcessMaxQuantity() { return totalProcessMaxQuantity; }
}
\ No newline at end of file
package com.aps.macroplanner.model;
import com.aps.macroplanner.data.TestDataBuilder;
import com.aps.macroplanner.variable.*;
/**
* 变量工厂 — 编排器,按构建顺序调度各变量构建器。
*
* <p>设计原则: 与 {@link com.aps.macroplanner.constraint.ConstraintFactory} 保持对称,
* 每个变量类别由独立的 Builder 类负责, 本类仅负责编排调度。</p>
*
* <h3>构建顺序</h3>
* <pre>
* 1. 生产变量 → ProductionVariableBuilder
* 2. 库存变量 → InventoryVariableBuilder
* 3. 需求变量 → DemandVariableBuilder
* 4. 产能松弛变量 → CapacityVariableBuilder
* 5. 库存松弛变量 → InventorySlackVariableBuilder
* 6. 供应松弛变量 → SupplySlackVariableBuilder
* 7. 批次松弛变量 → LotSizeVariableBuilder
* 8. BOM 依赖需求变量 → BomVariableBuilder
* 9. 需求满足量变量 → DemandFulfillmentVariableBuilder
* </pre>
*
* <h3>扩展方式</h3>
* 新增变量类型时:
* <ol>
* <li>在 {@code com.aps.macroplanner.variable} 包创建新的 Builder 类</li>
* <li>在 {@link MacroPlannerModel} 添加对应的变量 Map</li>
* <li>在本类的 {@link #createAll} 方法中注册调用</li>
* </ol>
*/
public class VariableFactory {
/**
* 创建所有决策变量并注册到模型。
*
* @param model 模型容器 (通过 getter 获取 Map 引用, 直接 put 变量)
* @param data 测试数据 (提供索引维度)
*/
public static void createAll(MacroPlannerModel model, TestDataBuilder data) {
ProductionVariableBuilder.create(model, data);
InventoryVariableBuilder.create(model, data);
DemandVariableBuilder.create(model, data);
CapacityVariableBuilder.create(model, data);
InventorySlackVariableBuilder.create(model, data);
SupplySlackVariableBuilder.create(model, data);
LotSizeVariableBuilder.create(model, data);
BomVariableBuilder.create(model, data);
DemandFulfillmentVariableBuilder.create(model, data);
}
}
\ No newline at end of file
package com.aps.macroplanner.objective;
import com.google.ortools.linearsolver.MPConstraint;
import com.google.ortools.linearsolver.MPObjective;
import com.google.ortools.linearsolver.MPSolver;
import com.aps.macroplanner.data.KPIWeights;
import com.aps.macroplanner.data.TestDataBuilder;
import com.aps.macroplanner.model.MacroPlannerModel;
/**
* 目标函数构建器 — 加权惩罚最小化
*
* <p>将多个 KPI 汇总变量按权重加权求和, 通过最小化该加权和来优化。
* 这是原始 Quintiq 模型中"非层级化目标"的等价实现。</p>
*
* <h3>数学公式</h3>
* <pre>
* Minimize Σ (weight × KPI_TotalVariable)
*
* 展开:
* obj = 100.0 × TotalFulfillment (需求满足 — 最高优先级)
* + 10.0 × TotalLotSize (批次偏差)
* + 5.0 × TotalMaxInventoryLevel (超库存)
* + 5.0 × TotalMinInventoryLevel (欠库存)
* + 8.0 × TotalTargetInvLevel (目标库存偏差)
* + 3.0 × TotalUnitCapacity (产能超载)
* + 8.0 × TotalSupplyTarget (供应目标偏差)
* + 5.0 × TotalMinSupply (最小供应不足)
* + 5.0 × TotalMaxSupply (最大供应超出)
* - 1.0 × TotalSalesDemandPriority (销售需求优先级 — 最大化, 加负号)
* + 20.0 × TotalPostponementPenalty (推迟惩罚 — 简化版=0)
* + 5.0 × TotalProcessMaxQuantity (过程最大量 — 简化版=0)
* </pre>
*
* <h3>方向说明</h3>
* 所有 KPI 汇总变量都是非负的"惩罚量", 所以目标是最小化。
* 唯一例外: TotalSalesDemandPriority 是"收益量" (越大越好),
* 所以使用负系数 (-1.0), 在最小化目标中等价于最大化。
*
* <h3>权重调优原则</h3>
* - 权重越大, 该项在目标函数中优先级越高
* - 例如: Fulfillment=100 远大于 LotSize=10,
* 优化器会优先满足需求, 即使这意味着批次偏差更大
* - 权重为 0 表示该项不影响目标 (但约束仍然存在)
*/
public class ObjectiveBuilder {
/**
* 构建目标函数。
* 必须在所有 KPI 汇总变量创建之后调用 (即 KpiAggregator.build() 之后)。
*
* @param model 模型容器 (提供所有 KPI 汇总变量)
* @param data 测试数据 (提供 KPI 权重)
*/
public static void build(MacroPlannerModel model, TestDataBuilder data) {
MPObjective objective = model.getSolver().objective();
KPIWeights w = data.getKpiWeights();
// ===== 惩罚项 (最小化: 正系数) =====
// Fulfillment: 需求松弛惩罚, 权重 100.0 (最高优先级)
// 含义: 所有周期所有产品的需求松弛总和。值越小表示需求满足越好。
objective.setCoefficient(model.getTotalFulfillment(), w.getFulfillmentWeight());
// LotSize: 批次偏差惩罚, 权重 10.0
// 含义: 所有操作所有周期的批次大小偏差总和。惩罚不按批次生产的量。
objective.setCoefficient(model.getTotalLotSize(), w.getLotSizeWeight());
// MaxInventoryLevel: 超库存惩罚, 权重 5.0
// 含义: 所有超标库存的总和。惩罚库存超过最大水平。
objective.setCoefficient(model.getTotalMaxInventoryLevel(), w.getMaxInventoryLevelWeight());
// MinInventoryLevel: 欠库存(安全库存)惩罚, 权重 5.0
// 含义: 所有低于最小库存的总和。惩罚库存低于安全水平。
objective.setCoefficient(model.getTotalMinInventoryLevel(), w.getMinInventoryLevelWeight());
// TargetInventoryLevel: 目标库存偏差惩罚, 权重 8.0
// 含义: 所有低于目标库存的总和。惩罚库存偏离目标水平。
objective.setCoefficient(model.getTotalTargetInvLevel(), w.getTargetInventoryLevelWeight());
// UnitCapacity: 产能超载惩罚, 权重 3.0
// 含义: 所有设备所有周期的产能超载总和。惩罚超负荷生产。
objective.setCoefficient(model.getTotalUnitCapacity(), w.getUnitCapacityWeight());
// SupplyTarget: 供应目标偏差惩罚, 权重 8.0
// 含义: 供应低于目标的总缺口。惩罚供应不足。
objective.setCoefficient(model.getTotalSupplyTarget(), w.getSupplyTargetWeight());
// MinSupply: 最小供应不足惩罚, 权重 5.0
// 含义: 供应低于最小值的总缺口。惩罚供应严重不足。
objective.setCoefficient(model.getTotalMinSupply(), w.getMinSupplyWeight());
// MaxSupply: 最大供应超出惩罚, 权重 5.0
// 含义: 供应超出最大值的总量。惩罚过度供应。
objective.setCoefficient(model.getTotalMaxSupply(), w.getMaxSupplyWeight());
// ===== 收益项 (最大化: 负系数, 因为 OR-Tools 默认最小化) =====
// SalesDemandPriority: 销售需求优先级, 权重 1.0, 负系数 = 最大化
// 含义: 按优先级加权的销售需求满足量。值越大表示满足的需求越多越好。
// 因目标函数是最小化, 此项使用负系数 (最大化 = 加负号后最小化)。
objective.setCoefficient(model.getTotalSalesDemandPriority(), -w.getSalesDemandPriorityWeight());
// PostponementPenalty: 推迟惩罚, 权重 20.0 (简化版固定为 0)
// 含义: 对应原模型中的延迟销售需求惩罚。当前版本未实现推迟逻辑。
objective.setCoefficient(model.getTotalPostponementPenalty(), w.getPostponementPenaltyWeight());
// ProcessMaxQuantity: 过程最大量惩罚, 权重 5.0 (简化版固定为 0)
// 含义: 对应原模型中超出最大过程量的惩罚。当前版本未实现。
objective.setCoefficient(model.getTotalProcessMaxQuantity(), w.getProcessMaxQuantityWeight());
// 设置为最小化目标
objective.setMinimization();
}
// ==================== 分层优化方法 ====================
/**
* 清除目标函数中所有 KPI 变量的系数 (置零)。
*
* <p>在分层优化中, 每次切换层级时需要先清除上一层的目标,
* 然后设置当前层的目标系数。OR-Tools 旧版 API 没有 clear() 方法,
* 因此显式将所有 KPI 系数置零。</p>
*
* @param model 模型容器
*/
public static void clearObjective(MacroPlannerModel model) {
MPObjective objective = model.getSolver().objective();
// 将所有 KPI 变量的系数置零
objective.setCoefficient(model.getTotalFulfillment(), 0.0);
objective.setCoefficient(model.getTotalLotSize(), 0.0);
objective.setCoefficient(model.getTotalMaxInventoryLevel(), 0.0);
objective.setCoefficient(model.getTotalMinInventoryLevel(), 0.0);
objective.setCoefficient(model.getTotalTargetInvLevel(), 0.0);
objective.setCoefficient(model.getTotalUnitCapacity(), 0.0);
objective.setCoefficient(model.getTotalSupplyTarget(), 0.0);
objective.setCoefficient(model.getTotalMinSupply(), 0.0);
objective.setCoefficient(model.getTotalMaxSupply(), 0.0);
objective.setCoefficient(model.getTotalSalesDemandPriority(), 0.0);
objective.setCoefficient(model.getTotalPostponementPenalty(), 0.0);
objective.setCoefficient(model.getTotalProcessMaxQuantity(), 0.0);
}
/**
* 设置当前层级的目标函数 — 只包含该层级的 KPI。
*
* <p>调用前应先调用 {@link #clearObjective(MacroPlannerModel)} 清除上层目标。</p>
*
* @param model 模型容器
* @param level 策略层级 (包含该层级的 KPI 列表)
*/
public static void setLevelObjective(MacroPlannerModel model, StrategyLevel level) {
MPObjective objective = model.getSolver().objective();
for (StrategyLevel.KPIEntry kpi : level.getKpis()) {
objective.setCoefficient(kpi.variable, kpi.effectiveCoefficient());
}
objective.setMinimization();
}
/**
* 添加层级边界约束 — 限制上层目标值不超过最优值 × (1 + slack)。
*
* <p>该约束确保在求解下层 KPI 时, 上层 KPI 不会退化超过允许范围。
* 对应 Quintiq 中 StrategyLevel 的 HierarchicalSolver 约束。</p>
*
* <h3>数学公式</h3>
* <pre>
* Σ (effectiveCoeff × KPI_variable) ≤ optimalValue × (1 + relativeGoalSlack)
* </pre>
*
* @param model 模型容器
* @param level 策略层级 (提供 KPI 列表和松弛比例)
* @param optimalValue 该层级在上一轮求解中的最优目标值
*/
public static void addLevelBoundConstraint(MacroPlannerModel model,
StrategyLevel level,
double optimalValue) {
if (level.getRelativeGoalSlack() < 0.0) return; // 负松弛表示不约束
MPSolver solver = model.getSolver();
double upperBound = optimalValue * (1.0 + level.getRelativeGoalSlack());
MPConstraint bound = solver.makeConstraint(
-MPSolver.infinity(), upperBound,
"HierLevel" + level.getLevel() + "_Bound");
for (StrategyLevel.KPIEntry kpi : level.getKpis()) {
bound.setCoefficient(kpi.variable, kpi.effectiveCoefficient());
}
}
}
\ No newline at end of file
This diff is collapsed.
package com.aps.macroplanner.util;
import com.aps.macroplanner.data.Period;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* 安全库存天数折算计算器 — 将 "N 天安全库存" 折算为对未来周期 DemandFulfillment 的加权求和。
*
* <h2>核心公式</h2>
* <pre>
* 安全库存天数 = N 天
* 折算结果: Σ(ratio_i × DemandFulfillment_i)
*
* 其中 ratio_i = min(remainingDays, p_i.duration) / p_i.duration
* </pre>
*
* <h2>使用场景</h2>
* <ul>
* <li>库存规格约束 (InventorySpec) — 最小/最大/目标库存以天为单位</li>
* <li>KPI 汇总 — 安全库存天数偏差计算</li>
* <li>结果分析 — 排查库存异常时追溯需求预测的来源</li>
* </ul>
*
* <h2>日志输出</h2>
* 当启用 FINE 级别日志时, 会输出每个周期的折算详情, 便于排查:
* <pre>
* [SafetyStock] PISPIP=PA_SP1, 当前周期=P0, 安全库存天数=3.00
* P1: 剩余 3.0天 → 消耗 1.0天/全覆盖周期(1.00) → 剩余 2.0天
* P2: 剩余 2.0天 → 消耗 1.0天/全覆盖周期(1.00) → 剩余 1.0天
* P3: 剩余 1.0天 → 消耗 1.0天/全覆盖周期(1.00) → 剩余 0.0天
* → 总覆盖天数: 3.0天, 涉及 3 个未来周期
* [SafetyStock] 折算结果: DFulfill_PA_SP1_1×1.00 + DFulfill_PA_SP1_2×1.00 + DFulfill_PA_SP1_3×1.00
* </pre>
*/
public class SafetyStockCalculator {
private static final Logger LOG = Logger.getLogger(SafetyStockCalculator.class.getName());
/**
* 计算安全库存天数对应的 DemandFulfillment 折算项。
*
* <p>从当前周期的下一个周期开始向后遍历, 直到覆盖完指定的安全库存天数,
* 或遍历完所有可用周期。每个未来周期按比例贡献其 DemandFulfillment。</p>
*
* @param currentPeriodIdx 当前周期在 periods 列表中的索引
* @param safetyStockDays 安全库存天数 (如 3.0 表示 3 天)
* @param periods 全部周期列表 (按时间顺序)
* @param invKey 当前库存的 key (productId_spId_periodIndex),
* 用于生成 DemandFulfillment 变量 key
* @return 折算项列表, 按周期顺序排列。如果所有未来周期都用完了仍不够覆盖,
* 返回已覆盖的项 (末尾周期可能 ratio < 1.0)
*/
public static List<SafetyStockTerm> calculate(
int currentPeriodIdx,
double safetyStockDays,
List<Period> periods,
String invKey) {
if (safetyStockDays <= 0) {
LOG.fine("[SafetyStock] 安全库存天数 <= 0, 跳过计算");
return Collections.emptyList();
}
List<SafetyStockTerm> terms = new ArrayList<>();
double remainingDays = safetyStockDays;
double totalCoveredDays = 0;
if (LOG.isLoggable(Level.FINE)) {
LOG.fine(String.format(
"[SafetyStock] PISPIP=%s, 当前周期=%s(%d), 安全库存天数=%.2f天, 开始折算...",
stripPeriodIndex(invKey), getPeriodName(periods, currentPeriodIdx),
currentPeriodIdx, safetyStockDays));
}
// 从下一个周期开始遍历
for (int i = currentPeriodIdx + 1; i < periods.size() && remainingDays > 0; i++) {
Period nextPeriod = periods.get(i);
double periodDuration = nextPeriod.getDurationInDays();
// ratio = 该周期对安全库存天数的贡献比例
// 如果剩余天数 >= 周期长度, 整个周期都算进去 (ratio = 1.0)
// 如果剩余天数 < 周期长度, 只算部分 (ratio = remainingDays / periodDuration)
double ratio = Math.min(remainingDays, periodDuration) / periodDuration;
double contributedDays = ratio * periodDuration;
double remainingBefore = remainingDays;
remainingDays -= periodDuration;
String dfKey = replacePeriodIndex(invKey, nextPeriod.getIndex());
SafetyStockTerm term = new SafetyStockTerm(
nextPeriod, ratio, remainingBefore, Math.max(0, remainingDays), dfKey);
terms.add(term);
totalCoveredDays += contributedDays;
LOG.fine(term.toString());
}
// 汇总日志
if (remainingDays > 0) {
LOG.warning(String.format(
"[SafetyStock] PISPIP=%s, 安全库存天数=%.2f天, 但未来周期不足! "
+ "已覆盖=%.2f天, 缺口=%.2f天 (未来只有 %d 个周期可用)",
stripPeriodIndex(invKey), safetyStockDays, totalCoveredDays,
remainingDays, periods.size() - currentPeriodIdx - 1));
} else {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine(String.format(
"[SafetyStock] PISPIP=%s → 总覆盖天数: %.2f天, 涉及 %d 个未来周期",
stripPeriodIndex(invKey), totalCoveredDays, terms.size()));
}
}
// 折算公式汇总
if (LOG.isLoggable(Level.FINE)) {
StringBuilder sb = new StringBuilder();
sb.append(String.format("[SafetyStock] 折算公式: InvQty ≥ "));
for (int i = 0; i < terms.size(); i++) {
if (i > 0) sb.append(" + ");
SafetyStockTerm t = terms.get(i);
sb.append(String.format("DFulfill_%s×%.2f",
t.getDemandFulfillmentKey(), t.getRatio()));
}
LOG.fine(sb.toString());
}
return terms;
}
/**
* 替换 key 中的周期索引。
* 例如: "PA_SP1_0" + 新索引 2 → "PA_SP1_2"
*/
static String replacePeriodIndex(String key, int newIndex) {
int lastUnderscore = key.lastIndexOf('_');
if (lastUnderscore < 0) return key;
return key.substring(0, lastUnderscore + 1) + newIndex;
}
/**
* 去掉 key 中的周期索引, 只保留 productId_spId。
* 例如: "PA_SP1_0" → "PA_SP1"
*/
private static String stripPeriodIndex(String key) {
int lastUnderscore = key.lastIndexOf('_');
if (lastUnderscore < 0) return key;
return key.substring(0, lastUnderscore);
}
/**
* 根据周期索引获取周期名称。
*/
private static String getPeriodName(List<Period> periods, int index) {
if (index >= 0 && index < periods.size()) {
return periods.get(index).getName();
}
return "?";
}
}
\ No newline at end of file
package com.aps.macroplanner.util;
import com.aps.macroplanner.data.Period;
/**
* 安全库存天数折算结果 — 单个周期项。
*
* <p>每次安全库存天数折算会在一个或多个未来周期上产生折算项。
* 每个项包含: 周期信息、折算比例、对应的 DemandFulfillment 变量 key。</p>
*
* <h3>示例</h3>
* 安全库存天数 = 10, 每周期 = 7 天, 当前周期 = P0:
* <pre>
* P1: ratio=1.0 (7/7, 全覆盖, remainingBefore=10, remainingAfter=3)
* P2: ratio=0.43 (3/7, 部分覆盖, remainingBefore=3, remainingAfter=0)
* </pre>
*/
public class SafetyStockTerm {
/** 周期索引 */
private final int periodIndex;
/** 周期名称 */
private final String periodName;
/** 周期长度 (天) */
private final double periodDuration;
/** 折算比例: 该周期贡献了多少天到安全库存中 */
private final double ratio;
/** 折算前剩余天数 */
private final double remainingDaysBefore;
/** 折算后剩余天数 */
private final double remainingDaysAfter;
/** DemandFulfillment 变量 key (productId_spId_periodIndex) */
private final String demandFulfillmentKey;
public SafetyStockTerm(Period period, double ratio,
double remainingDaysBefore, double remainingDaysAfter,
String demandFulfillmentKey) {
this.periodIndex = period.getIndex();
this.periodName = period.getName();
this.periodDuration = period.getDurationInDays();
this.ratio = ratio;
this.remainingDaysBefore = remainingDaysBefore;
this.remainingDaysAfter = remainingDaysAfter;
this.demandFulfillmentKey = demandFulfillmentKey;
}
public int getPeriodIndex() { return periodIndex; }
public String getPeriodName() { return periodName; }
public double getPeriodDuration() { return periodDuration; }
public double getRatio() { return ratio; }
public double getRemainingDaysBefore() { return remainingDaysBefore; }
public double getRemainingDaysAfter() { return remainingDaysAfter; }
public String getDemandFulfillmentKey() { return demandFulfillmentKey; }
/**
* 该周期对安全库存天数的实际贡献天数。
*/
public double getContributionDays() {
return ratio * periodDuration;
}
@Override
public String toString() {
String coverage = (ratio >= 0.999) ? "全覆盖" : String.format("部分(%.1f%%)", ratio * 100);
return String.format(" %s: 剩余 %5.1f天 → 消耗 %5.1f天/%s 周期(%s) → 剩余 %5.1f天",
periodName, remainingDaysBefore, getContributionDays(), coverage,
(ratio >= 0.999) ? "1.00" : String.format("%.2f", ratio),
remainingDaysAfter);
}
}
\ No newline at end of file
package com.aps.macroplanner.variable;
import com.google.ortools.linearsolver.MPSolver;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
/**
* BOM 依赖需求变量构建器 — 创建 OperationDemandQty 和 DependentDemandInPISPIP 变量。
*
* <h3>OperationDemandQty[inputKey_periodIndex]</h3>
* <ul>
* <li>含义: 某操作在某周期对某输入物料的消耗量</li>
* <li>范围: [0, +∞)</li>
* </ul>
*
* <h3>DependentDemandInPISPIP[productId_spId_periodIndex]</h3>
* <ul>
* <li>含义: 某个产品/库存点/周期被所有操作作为原材料消耗的总量</li>
* <li>范围: [0, +∞)</li>
* </ul>
*/
public class BomVariableBuilder {
public static void create(MacroPlannerModel model, TestDataBuilder data) {
double inf = MPSolver.infinity();
// OperationDemandQty[inputKey, period]
for (OperationInput input : data.getOperationInputs()) {
for (Period p : data.getPeriods()) {
String key = input.getKey() + "_" + p.getIndex();
model.getOperationDemandQtyVars().put(key,
model.getSolver().makeNumVar(0.0, inf, "OpDemand_" + key));
}
}
// DependentDemandInPISPIP[productId, spId, period]
for (Product prod : data.getProducts()) {
for (StockingPoint sp : data.getStockingPointsForProduct(prod.getId())) {
for (Period p : data.getPeriods()) {
String key = prod.getId() + "_" + sp.getId() + "_" + p.getIndex();
model.getDependentDemandVars().put(key,
model.getSolver().makeNumVar(0.0, inf, "DepDemand_" + key));
}
}
}
}
}
\ No newline at end of file
package com.aps.macroplanner.variable;
import com.google.ortools.linearsolver.MPSolver;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
/**
* 产能松弛变量构建器 — 创建 CapacityOverloaded 和 CapacityNotMet 变量。
*
* <h3>CapacityOverloaded[unitId_periodIndex]</h3>
* <ul>
* <li>含义: 设备产能超出最大限制的量</li>
* <li>范围: [0, +∞)</li>
* </ul>
*
* <h3>CapacityNotMet[unitId_periodIndex]</h3>
* <ul>
* <li>含义: 设备产能低于最小使用的量</li>
* <li>范围: [0, +∞)</li>
* </ul>
*/
public class CapacityVariableBuilder {
public static void create(MacroPlannerModel model, TestDataBuilder data) {
double inf = MPSolver.infinity();
for (UnitPeriod up : data.getUnitPeriods()) {
String key = up.getKey();
model.getCapacityOverloadedVars().put(key,
model.getSolver().makeNumVar(0.0, inf, "CapOver_" + key));
model.getCapacityNotMetVars().put(key,
model.getSolver().makeNumVar(0.0, inf, "CapNotMet_" + key));
}
}
}
\ No newline at end of file
package com.aps.macroplanner.variable;
import com.google.ortools.linearsolver.MPSolver;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
/**
* 需求满足量变量构建器 — 创建 DemandFulfillmentInPISPIP 变量。
*
* <p>DemandFulfillmentInPISPIP[productId_spId_periodIndex] — PISPIP 总需求满足量</p>
* <ul>
* <li>含义: 某个产品/库存点/周期内所有需求的总满足量</li>
* <li>用途: 安全库存天数计算的基础 — 库存 ≥ 未来N天需求满足量</li>
* <li>组成: SalesDemandQty + OperationDemandQty (BOM消耗)</li>
* <li>范围: [0, +∞)</li>
* </ul>
*/
public class DemandFulfillmentVariableBuilder {
public static void create(MacroPlannerModel model, TestDataBuilder data) {
double inf = MPSolver.infinity();
for (Product prod : data.getProducts()) {
for (StockingPoint sp : data.getStockingPointsForProduct(prod.getId())) {
for (Period p : data.getPeriods()) {
String key = prod.getId() + "_" + sp.getId() + "_" + p.getIndex();
model.getDemandFulfillmentVars().put(key,
model.getSolver().makeNumVar(0.0, inf, "DFulfill_" + key));
}
}
}
}
}
\ No newline at end of file
package com.aps.macroplanner.variable;
import com.google.ortools.linearsolver.MPSolver;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
/**
* 需求变量构建器 — 创建 SalesDemandQty 和 DemandSlack 变量。
*
* <h3>SalesDemandQty[productId_spId_periodIndex]</h3>
* <ul>
* <li>索引: SalesDemand (Product × StockingPoint × Period)</li>
* <li>含义: 实际满足的独立销售需求量 (≤ 原始需求总量)</li>
* <li>范围: [0, 需求总量]</li>
* </ul>
*
* <h3>DemandSlack[productId_spId_periodIndex]</h3>
* <ul>
* <li>索引: Product × StockingPoint × Period</li>
* <li>含义: 物料平衡约束中的松弛量,用于防止模型不可行。
* 当供应无法满足需求时,可由 DemandSlack "虚拟供应"来填补缺口。</li>
* <li>范围: [0, +∞)</li>
* <li>在目标函数中以高权重惩罚,确保只在必要时使用</li>
* </ul>
*/
public class DemandVariableBuilder {
public static void create(MacroPlannerModel model, TestDataBuilder data) {
createSalesDemandQty(model, data);
createDemandSlack(model, data);
}
private static void createSalesDemandQty(MacroPlannerModel model, TestDataBuilder data) {
for (SalesDemand sd : data.getSalesDemands()) {
model.getSalesDemandQtyVars().put(sd.getKey(),
model.getSolver().makeNumVar(0.0, sd.getQuantity(), "SDQty_" + sd.getKey()));
}
}
private static void createDemandSlack(MacroPlannerModel model, TestDataBuilder data) {
double inf = MPSolver.infinity();
for (Product prod : data.getProducts()) {
for (StockingPoint sp : data.getStockingPointsForProduct(prod.getId())) {
for (Period p : data.getPeriods()) {
String key = prod.getId() + "_" + sp.getId() + "_" + p.getIndex();
model.getDemandSlackVars().put(key,
model.getSolver().makeNumVar(0.0, inf, "DemandSlack_" + key));
}
}
}
}
}
\ No newline at end of file
package com.aps.macroplanner.variable;
import com.google.ortools.linearsolver.MPSolver;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
/**
* 库存松弛变量构建器 — 创建 MinInvQtyUnder, MaxInvQtyOver, InvQtyUnderTarget 变量。
*
* <p>仅在对应库存规格定义时才创建对应的松弛变量
* (例如: 没有定义 MaxLevel 则不创建 MaxInvQtyOver)。</p>
*
* <h3>MinInvQtyUnder[productId_spId_periodIndex]</h3>
* <ul><li>含义: 低于最小库存的量</li><li>范围: [0, +∞)</li></ul>
*
* <h3>MaxInvQtyOver[productId_spId_periodIndex]</h3>
* <ul><li>含义: 超过最大库存的量</li><li>范围: [0, +∞)</li></ul>
*
* <h3>InvQtyUnderTarget[productId_spId_periodIndex]</h3>
* <ul><li>含义: 低于目标库存的量</li><li>范围: [0, +∞)</li></ul>
*/
public class InventorySlackVariableBuilder {
public static void create(MacroPlannerModel model, TestDataBuilder data) {
double inf = MPSolver.infinity();
for (InventorySpec spec : data.getInventorySpecs()) {
String key = spec.getKey();
if (spec.hasMinLevel() || spec.hasMinLevelInDays()) {
model.getMinInvQtyUnderVars().put(key,
model.getSolver().makeNumVar(0.0, inf, "MinInvUnder_" + key));
}
if (spec.hasMaxLevel() || spec.hasMaxLevelInDays()) {
model.getMaxInvQtyOverVars().put(key,
model.getSolver().makeNumVar(0.0, inf, "MaxInvOver_" + key));
}
if (spec.hasTarget() || spec.hasTargetInDays()) {
model.getInvQtyUnderTargetVars().put(key,
model.getSolver().makeNumVar(0.0, inf, "InvUnderTarget_" + key));
}
}
}
}
\ No newline at end of file
package com.aps.macroplanner.variable;
import com.google.ortools.linearsolver.MPSolver;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
/**
* 库存变量构建器 — 创建 InvQty 变量。
*
* <p>InvQty[productId_spId_periodIndex] — 期末库存变量</p>
* <ul>
* <li>索引: Product × StockingPoint × Period</li>
* <li>含义: 某个产品在某个库存点、某个周期结束时的库存量</li>
* <li>范围: [0, +∞)</li>
* <li>注意: 第 0 周期的期初库存由初始库存常量给定,
* 之后每个周期的期初库存 = 上一周期的期末库存 (由 Balance 约束保证)</li>
* </ul>
*/
public class InventoryVariableBuilder {
public static void create(MacroPlannerModel model, TestDataBuilder data) {
double inf = MPSolver.infinity();
for (Product prod : data.getProducts()) {
for (StockingPoint sp : data.getStockingPointsForProduct(prod.getId())) {
for (Period p : data.getPeriods()) {
String key = prod.getId() + "_" + sp.getId() + "_" + p.getIndex();
model.getInvQtyVars().put(key,
model.getSolver().makeNumVar(0.0, inf, "InvQty_" + key));
}
}
}
}
}
\ No newline at end of file
package com.aps.macroplanner.variable;
import com.google.ortools.linearsolver.MPSolver;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
/**
* 批次松弛变量构建器 — 创建 PTLotSizeOver 和 PTLotSizeUnder 变量。
*
* <p>仅在操作定义了批次大小时创建。</p>
*
* <h3>PTLotSizeOver[operationId_unitId_periodIndex]</h3>
* <ul><li>含义: 生产量超出批次大小的量</li><li>范围: [0, +∞)</li></ul>
*
* <h3>PTLotSizeUnder[operationId_unitId_periodIndex]</h3>
* <ul><li>含义: 生产量不足批次大小的量</li><li>范围: [0, +∞)</li></ul>
*/
public class LotSizeVariableBuilder {
public static void create(MacroPlannerModel model, TestDataBuilder data) {
double inf = MPSolver.infinity();
for (Operation op : data.getOperations()) {
for (UnitOperation uo : op.getUnitOperations()) {
if (!uo.hasLotSize()) continue;
for (Period p : data.getPeriods()) {
String key = op.ptQtyKey(uo, p.getIndex());
model.getPtLotSizeOverVars().put(key,
model.getSolver().makeNumVar(0.0, inf, "LotOver_" + key));
model.getPtLotSizeUnderVars().put(key,
model.getSolver().makeNumVar(0.0, inf, "LotUnder_" + key));
}
}
}
}
}
\ No newline at end of file
package com.aps.macroplanner.variable;
import com.google.ortools.linearsolver.MPSolver;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
/**
* 生产变量构建器 — 创建 PTQty 变量。
*
* <p>PTQty[operationId_unitId_periodIndex] — 生产量变量</p>
* <ul>
* <li>索引: Operation × UnitOperation × Period</li>
* <li>含义: 某个操作在某个单元、某个周期内的生产数量</li>
* <li>范围: [0, +∞)</li>
* </ul>
*/
public class ProductionVariableBuilder {
public static void create(MacroPlannerModel model, TestDataBuilder data) {
double inf = MPSolver.infinity();
for (Operation op : data.getOperations()) {
for (UnitOperation uo : op.getUnitOperations()) {
for (Period p : data.getPeriods()) {
String key = op.ptQtyKey(uo, p.getIndex());
model.getPtQtyVars().put(key,
model.getSolver().makeNumVar(0.0, inf, "PTQty_" + key));
}
}
}
}
}
\ No newline at end of file
package com.aps.macroplanner.variable;
import com.google.ortools.linearsolver.MPSolver;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Logger;
/**
* 供应松弛变量构建器 — 创建 SupplyTargetQtyUnder, MinSupplyQtyUnder, MaxSupplyQtyOver 变量。
*
* <h3>SupplyTargetQtyUnder[specName]</h3>
* <ul><li>含义: 供应目标不足的量</li><li>范围: [0, +∞)</li></ul>
*
* <h3>MinSupplyQtyUnder[specName]</h3>
* <ul><li>含义: 最小供应不足的量</li><li>范围: [0, +∞)</li></ul>
*
* <h3>MaxSupplyQtyOver[specName]</h3>
* <ul><li>含义: 最大供应超出的量</li><li>范围: [0, +∞)</li></ul>
*
* <p><b>注意:</b> 每个供应规格必须有唯一的名称, 否则会导致松弛变量被覆盖,
* 多个约束共享同一个松弛变量, 造成数学模型错误。</p>
*/
public class SupplySlackVariableBuilder {
private static final Logger LOG = Logger.getLogger(SupplySlackVariableBuilder.class.getName());
public static void create(MacroPlannerModel model, TestDataBuilder data) {
double inf = MPSolver.infinity();
Set<String> seenNames = new HashSet<>();
for (SupplySpec spec : data.getSupplySpecs()) {
String name = spec.getName();
// 检测重复名称: 如果已存在则覆盖, 导致多个约束共享同一个松弛变量
if (!seenNames.add(name)) {
LOG.warning(String.format(
"⚠️ 供应规格名称重复: '%s' 已存在! "
+ "这将导致不同供应规格的约束共享同一个松弛变量, 造成数学模型错误。"
+ "请确保每个 SupplySpec 有唯一的名称。",
name));
}
model.getSupplyTargetQtyUnderVars().put(name,
model.getSolver().makeNumVar(0.0, inf, "SupTargetUnder_" + name));
model.getMinSupplyQtyUnderVars().put(name,
model.getSolver().makeNumVar(0.0, inf, "MinSupUnder_" + name));
if (spec.hasMaxQuantity()) {
model.getMaxSupplyQtyOverVars().put(name,
model.getSolver().makeNumVar(0.0, inf, "MaxSupOver_" + name));
}
}
}
}
\ No newline at end of file
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