Commit 48800682 authored by Tong Li's avatar Tong Li

MP

parent 74589725
......@@ -119,7 +119,7 @@
<dependency>
<groupId>com.google.ortools</groupId>
<artifactId>ortools-java</artifactId>
<version>9.7.2996</version>
<version>9.15.6755</version>
</dependency>
<!-- HTTP客户端 (用于调用LLM API) -->
......
......@@ -56,4 +56,23 @@ public class FileHelper {
System.err.println("Failed to write log: " + e.getMessage());
}
}
public static void writeFile(String message,String fileName) {
String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))+"-";
// 确保目录存在
java.io.File logDir = new java.io.File(LOG_FILE_PATH);
if (!logDir.exists()) {
logDir.mkdirs(); // 创建目录(包括父目录)
}
String filePath = LOG_FILE_PATH + date + fileName;
try (PrintWriter writer = new PrintWriter(new FileWriter(filePath, true))) {
writer.print(message);
} catch (IOException e) {
System.err.println("Failed to write log: " + e.getMessage());
}
}
}
\ No newline at end of file
package com.aps.common.util;
import java.io.OutputStream;
import java.io.PrintStream;
/**
* 作者:佟礼
* 时间:2026-07-24
*/
public class TeePrintStream extends PrintStream {
private final PrintStream other;
public TeePrintStream(OutputStream main, PrintStream other) {
super(main);
this.other = other;
}
@Override
public void write(int b) {
super.write(b);
other.write(b);
}
@Override
public void write(byte[] buf, int off, int len) {
super.write(buf, off, len);
other.write(buf, off, len);
}
@Override
public void flush() {
super.flush();
other.flush();
}
@Override
public void close() {
super.close();
other.close();
}
}
package com.aps.macroplanner;
import com.google.ortools.Loader;
import com.google.ortools.linearsolver.MPSolver;
import com.aps.macroplanner.constraint.ConstraintFactory;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
import com.aps.macroplanner.model.VariableFactory;
import com.aps.macroplanner.objective.ObjectiveBuilder;
import com.aps.macroplanner.objective.StrategyLevel;
import com.aps.macroplanner.output.SolutionPrinter;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* MacroPlanner 核心优化器 — 供应链产能规划混合整数规划(MIP)模型
*
* 将 Quintiq CapacityPlanningSuboptimizer 的核心模型迁移到 Google OR-Tools Java API。
*
* <h2>模型概述</h2>
* <pre>
* 优化目标: 在满足物料平衡、产能、库存、供应、批次等约束的前提下,
* 最小化加权惩罚项 (需求缺口、库存偏差、产能超载、批次偏差等) 的加权和。
*
* 决策变量 (14 类):
* PTQty — 生产量
* InvQty — 期末库存
* SalesDemandQty — 销售需求满足量
* DemandSlack — 需求松弛 (防止不可行)
* OperationDemandQty — BOM 依赖需求 (原材料消耗)
* DependentDemandInPISPIP— PISPIP 总依赖需求
* 产能松弛 (CapacityOverloaded, CapacityNotMet)
* 库存松弛 (MinInvQtyUnder, MaxInvQtyOver, InvQtyUnderTarget)
* 供应松弛 (SupplyTargetQtyUnder, MinSupplyQtyUnder, MaxSupplyQtyOver)
* 批次松弛 (PTLotSizeOver, PTLotSizeUnder)
*
* 约束 (7 类):
* 物料平衡 — 每周期流入 = 流出
* BOM 依赖需求— 生产消耗 = 产量 × BOM 因子
* 产能 — 设备产能上下限
* 库存规格 — 最小/最大/目标库存
* 供应规格 — 最小/最大/目标供应
* 批次大小 — 批量生产约束
* KPI 汇总 — 各松弛变量汇总到全局 KPI
*
* 目标函数:
* 最小化 Σ (KPI权重 × KPI汇总变量)
* </pre>
*
* <h2>模块架构</h2>
* <pre>
* MacroPlannerOptimizer (编排器 — 流程控制)
* ├── MacroPlannerModel (模型容器 — 求解器 + 所有变量)
* ├── VariableFactory (变量工厂 — 创建所有决策变量)
* ├── ConstraintFactory (约束工厂 — 统一调度所有约束构建)
* │ ├── BalanceConstraint (物料平衡约束)
* │ ├── BomConstraint (BOM 依赖需求约束)
* │ ├── CapacityConstraint (产能约束)
* │ ├── InventorySpecConstraint (库存规格约束)
* │ ├── SupplySpecConstraint (供应规格约束)
* │ ├── LotSizeConstraint (批次大小约束)
* │ └── KpiAggregator (KPI 汇总变量)
* └── ObjectiveBuilder (目标函数)
* </pre>
*
* <h2>测试场景</h2>
* 两级供应链: ProductA(成品) ← OP1 生产, 消耗 ProductB(原材料) × 1.0 ← OP2 生产
* 4 个周期, 2 个产品, 2 个操作, 共享 1 台设备 (Unit1, 最大产能 200/周期)
*/
public class MacroPlannerOptimizer {
/** LP 模型文件和日志文件的输出目录 */
private static final String LOG_DIR = "src/main/java/com/aps/log/";
/** LP 模型文件路径 */
private static final String LP_FILE_PATH = LOG_DIR + "model.lp";
/** 运行日志文件路径 */
private static final String LOG_FILE_PATH = LOG_DIR + "log.txt";
// ==================== 核心组件 ====================
/** 模型容器 — 持有求解器、所有决策变量和 KPI 汇总变量 */
private final MacroPlannerModel model;
/** 测试数据构建器, 包含所有输入数据 (产品、操作、BOM、需求、库存规格等) */
private final TestDataBuilder data;
/**
* 构造优化器实例 (使用默认简单测试数据)。
*/
public MacroPlannerOptimizer() {
this(new TestDataBuilder());
}
/**
* 构造优化器实例 (使用自定义测试数据)。
* @param data 测试数据构建器 (支持子类如 ComprehensiveTestDataBuilder)
*/
public MacroPlannerOptimizer(TestDataBuilder data) {
this.data = data;
this.model = new MacroPlannerModel();
}
/**
* 配置日志级别, 输出安全库存天数折算的详细过程。
*
* <p>INFO 级别: 输出每个安全库存约束的构建摘要</p>
* <p>FINE 级别: 输出每个周期的折算详情 + 折算公式</p>
* <p>设为 Level.OFF 可关闭日志</p>
*/
private static void configureLogging() {
// 配置根 Logger 使用 ConsoleHandler
Logger rootLogger = Logger.getLogger("");
rootLogger.setLevel(Level.INFO);
// 清除默认 handler, 使用自定义格式
for (java.util.logging.Handler h : rootLogger.getHandlers()) {
rootLogger.removeHandler(h);
}
ConsoleHandler handler = new ConsoleHandler();
handler.setLevel(Level.ALL);
handler.setFormatter(new java.util.logging.SimpleFormatter() {
@Override
public synchronized String format(java.util.logging.LogRecord record) {
return String.format(" [%s] %s%n",
record.getLevel().getLocalizedName(), record.getMessage());
}
});
rootLogger.addHandler(handler);
}
// ==================== 模型构建流程 ====================
/**
* 构建完整的优化模型。
*
* 构建顺序 (通过工厂类统一调度, MacroPlannerModel 协作):
* 1. 创建所有决策变量 → VariableFactory.createAll()
* 2. 创建所有约束和 KPI 汇总 → ConstraintFactory.buildAll()
* 3. 创建目标函数 (加权求和) → ObjectiveBuilder.build()
*/
public void buildModel() {
// 启用安全库存天数的详细日志 (INFO 级别)
// 设为 FINE 可输出每个周期的折算详情
configureLogging();
System.out.println("=== 开始构建 MacroPlanner 优化模型 ===\n");
// 0. 数据完整性检查 (在构建模型前验证)
DataValidator validator = new DataValidator(data);
if (!validator.validate()) {
System.out.println(" ⚠️ 数据检查发现错误, 求解结果可能不可靠\n");
} else {
System.out.println(" [OK] 数据检查通过\n");
}
// 1. 决策变量
VariableFactory.createAll(model, data);
System.out.println(" [OK] 决策变量创建完成");
// 2. 约束 + KPI 汇总 (由 ConstraintFactory 统一调度)
ConstraintFactory.buildAll(model, data);
System.out.println(" [OK] 约束与KPI汇总创建完成");
// 3. 目标函数 (加权求和)
ObjectiveBuilder.build(model, data);
System.out.println(" [OK] 目标函数创建完成");
// 4. 导出 LP 模型文件
exportLpModel();
System.out.println("\n模型统计: 变量=" + model.getSolver().numVariables()
+ ", 约束=" + model.getSolver().numConstraints() + "\n");
}
/** 求解开始时间 (用于计算耗时) */
private long startTimeMs;
/**
* 导出 LP 模型文件到磁盘。
*
* <p>LP 文件包含完整的线性规划模型: 目标函数、所有变量和约束,
* 可用 LP 求解器 (如 Gurobi、CPLEX) 直接读取,
* 便于调试、验证和审计模型结构。</p>
*/
private void exportLpModel() {
try {
Path logDir = Paths.get(LOG_DIR);
if (!Files.exists(logDir)) {
Files.createDirectories(logDir);
}
String lpContent = model.getSolver().exportModelAsLpFormat();
Path lpPath = Paths.get(LP_FILE_PATH).toAbsolutePath();
Files.write(lpPath, lpContent.getBytes(StandardCharsets.UTF_8));
System.out.println(" [OK] LP模型文件已导出: " + lpPath);
} catch (Exception e) {
System.err.println(" [WARN] LP模型导出失败: " + e.getMessage());
}
}
// ==================== 求解 ====================
/**
* 分层优化结果记录。
*/
private static class LevelResult {
final StrategyLevel level;
final double optimalValue;
final MPSolver.ResultStatus status;
LevelResult(StrategyLevel level, double optimalValue, MPSolver.ResultStatus status) {
this.level = level;
this.optimalValue = optimalValue;
this.status = status;
}
}
/** 各层级求解结果 (solve() 填充) */
private final List<LevelResult> levelResults = new ArrayList<>();
/**
* 执行分层优化求解。
*
* <h3>分层优化流程</h3>
* <pre>
* 1. 定义策略层级 (需求 → 产能 → 业务KPI → 软约束)
* 2. 逐层求解:
* a) 清除上层目标, 设置当前层目标
* b) 调用 solver.solve()
* c) 记录最优值
* d) 添加边界约束: 上层目标 ≤ 最优值 × (1 + slack)
* 3. 输出最终结果 (最后一层的解为最终解)
* </pre>
*
* <p>该实现对应 Quintiq 中 StrategyLevel 的 HierarchicalSolver 机制。
* 每个层级独立求解, 上层最优值作为下层约束, 确保严格优先级顺序。</p>
*/
public void solve() {
System.out.println("=== 开始分层求解 ===\n");
startTimeMs = System.currentTimeMillis();
KPIWeights w = data.getKpiWeights();
List<StrategyLevel> levels = defineLevels(w);
levelResults.clear();
for (int i = 0; i < levels.size(); i++) {
StrategyLevel level = levels.get(i);
if (!level.hasKpis()) continue;
System.out.printf("--- 第 %d/%d 层: %s (松弛=%.0f%%) ---%n",
i + 1, levels.size(), level.getName(),
level.getRelativeGoalSlack() * 100);
// 清除上层目标, 设置当前层目标
ObjectiveBuilder.clearObjective(model);
ObjectiveBuilder.setLevelObjective(model, level);
// 求解 (计时)
long levelStartMs = System.currentTimeMillis();
final MPSolver.ResultStatus status = model.getSolver().solve();
long levelElapsedMs = System.currentTimeMillis() - levelStartMs;
double optimalValue = model.getSolver().objective().value();
// SCIP 风格求解摘要
System.out.printf(" SCIP Status : %s%n", status);
System.out.printf(" Solving Time (sec) : %.2f%n", levelElapsedMs / 1000.0);
System.out.printf(" Primal Bound : %+.6e%n", optimalValue);
// 输出当前层各 KPI 值
for (StrategyLevel.KPIEntry kpi : level.getKpis()) {
double kpiValue = kpi.variable.solutionValue();
double penalty = kpi.effectiveCoefficient() * kpiValue;
System.out.printf(" %s: %.2f (系数=%.1f, 惩罚=%.2f)%n",
kpi.name, kpiValue, kpi.effectiveCoefficient(), penalty);
}
levelResults.add(new LevelResult(level, optimalValue, status));
// 添加边界约束 (最后一层不需要)
if (i < levels.size() - 1 && level.getRelativeGoalSlack() >= 0.0) {
ObjectiveBuilder.addLevelBoundConstraint(model, level, optimalValue);
System.out.printf(" 已添加边界约束: 上层目标 ≤ %.2f%n",
optimalValue * (1.0 + level.getRelativeGoalSlack()));
}
System.out.println();
}
// 输出最终结果
MPSolver.ResultStatus finalStatus = model.getSolver().solve();
if (finalStatus == MPSolver.ResultStatus.OPTIMAL
|| finalStatus == MPSolver.ResultStatus.FEASIBLE) {
SolutionPrinter printer = new SolutionPrinter(model, data, startTimeMs);
printer.printAll();
// 构建层级最优值列表
List<Double> levelObjValues = new ArrayList<>();
for (LevelResult r : levelResults) {
levelObjValues.add(r.optimalValue);
}
printer.printHierarchicalSummary(levels, levelObjValues);
} else {
System.out.println("求解失败! 状态: " + finalStatus);
}
}
/**
* 定义策略层级 — 将 KPI 按优先级分组。
*
* <h3>层级划分</h3>
* <pre>
* Level 1 (需求满足): Fulfillment — 需求缺口必须最小化
* Level 2 (产能): UnitCapacity — 物理产能约束
* Level 3 (业务KPI): LotSize, TargetInventory, SupplyTarget, SalesPriority
* Level 4 (软约束): MaxInventory, MinInventory, MinSupply, MaxSupply
* </pre>
*
* @param w KPI 权重配置
* @return 策略层级列表 (按优先级排序)
*/
private List<StrategyLevel> defineLevels(KPIWeights w) {
List<StrategyLevel> levels = new ArrayList<>();
// === Level 1: 需求满足 (最高优先级, 严格分层, slack=0%) ===
StrategyLevel l1 = new StrategyLevel(1, "需求满足", 0.0);
l1.addKPI("需求缺口", model.getTotalFulfillment(), w.getFulfillmentWeight());
levels.add(l1);
// === Level 2: 产能约束 (物理硬约束, 严格分层, slack=0%) ===
StrategyLevel l2 = new StrategyLevel(2, "产能约束", 0.0);
l2.addKPI("产能超载", model.getTotalUnitCapacity(), w.getUnitCapacityWeight());
levels.add(l2);
// === Level 3: 业务KPI (允许 5% 退化, slack=5%) ===
StrategyLevel l3 = new StrategyLevel(3, "业务KPI", 0.05);
l3.addKPI("批次偏差", model.getTotalLotSize(), w.getLotSizeWeight());
l3.addKPI("目标库存偏差", model.getTotalTargetInvLevel(), w.getTargetInventoryLevelWeight());
l3.addKPI("供应目标偏差", model.getTotalSupplyTarget(), w.getSupplyTargetWeight());
l3.addKPI("销售优先级", model.getTotalSalesDemandPriority(),
w.getSalesDemandPriorityWeight(), true); // 负系数 = 最大化
levels.add(l3);
// === Level 4: 软约束 (允许 10% 退化, slack=10%) ===
StrategyLevel l4 = new StrategyLevel(4, "软约束", 0.10);
l4.addKPI("超库存", model.getTotalMaxInventoryLevel(), w.getMaxInventoryLevelWeight());
l4.addKPI("欠库存", model.getTotalMinInventoryLevel(), w.getMinInventoryLevelWeight());
l4.addKPI("最小供应不足", model.getTotalMinSupply(), w.getMinSupplyWeight());
l4.addKPI("最大供应超出", model.getTotalMaxSupply(), w.getMaxSupplyWeight());
levels.add(l4);
return levels;
}
// ==================== 主入口 ====================
/**
* 程序入口。
* 执行流程:
* 1. 加载 OR-Tools 本地库 (JNI)
* 2. 创建优化器实例
* 3. 构建模型 (变量 + 约束 + 目标)
* 4. 导出 LP 模型文件
* 5. 求解并输出结果 (同时写入日志文件)
*/
public static void main(String[] args) {
// 准备日志目录
try {
Path logDir = Paths.get(LOG_DIR);
if (!Files.exists(logDir)) {
Files.createDirectories(logDir);
}
} catch (Exception e) {
System.err.println("无法创建日志目录: " + e.getMessage());
}
// 将控制台输出同时写入日志文件 (Tee模式)
PrintStream originalOut = System.out;
try {
String logPath = Paths.get(LOG_FILE_PATH).toAbsolutePath().toString();
PrintStream fileOut = new PrintStream(new FileOutputStream(logPath));
PrintStream teeOut = new TeePrintStream(originalOut, fileOut);
System.setOut(teeOut);
System.setErr(teeOut);
} catch (Exception e) {
System.err.println("无法创建日志文件: " + e.getMessage());
}
// 输出运行时间戳
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
System.out.println("=== MacroPlanner 优化器运行日志 ===");
System.out.println("=== 运行时间: " + timestamp + " ===\n");
// 加载 OR-Tools 本地库 (包含 SCIP 求解器的 C++ 实现)
Loader.loadNativeLibraries();
MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer();
optimizer.buildModel();
optimizer.solve();
System.out.println("=== 运行结束 ===");
// 恢复原始 System.out
System.setOut(originalOut);
System.setErr(originalOut);
}
/**
* TeePrintStream — 将输出同时写入两个 PrintStream (控制台 + 文件)。
*/
private static class TeePrintStream extends PrintStream {
private final PrintStream secondary;
TeePrintStream(PrintStream primary, PrintStream secondary) {
super(primary);
this.secondary = secondary;
}
@Override
public void write(int b) {
super.write(b);
secondary.write(b);
}
@Override
public void write(byte[] buf, int off, int len) {
super.write(buf, off, len);
secondary.write(buf, off, len);
}
@Override
public void flush() {
super.flush();
secondary.flush();
}
@Override
public void close() {
super.close();
secondary.close();
}
}
}
\ No newline at end of file
package com.aps.macroplanner;
import com.google.ortools.Loader;
import com.aps.macroplanner.data.RoutingTestDataBuilder;
import com.aps.macroplanner.data.TestDataBuilder;
/**
* 多工序路由测试运行器
*
* <p>验证场景: 3 工序串行生产同一产品, 验证:
* <ol>
* <li>最终产出 = 销售需求 (不是 3×)</li>
* <li>中间 WIP 库存不异常累积</li>
* <li>各工序独立消耗各自设备产能</li>
* </ol>
*/
public class RoutingTestRunner {
public static void main(String[] args) {
Loader.loadNativeLibraries();
System.out.println("===== ROUTING TEST RUNNER START =====");
System.out.println("Scenario: 3-step routing OP_Cut -> OP_Rough -> OP_Finish -> Product P");
System.out.println();
TestDataBuilder data = new RoutingTestDataBuilder();
System.out.println("Data loaded: " + data.getProducts().size() + " products, "
+ data.getOperations().size() + " operations");
MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
optimizer.buildModel();
optimizer.solve();
System.out.println("===== ROUTING TEST RUNNER END =====");
}
}
\ 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;
/**
* 需求缺口联动约束 (DemandSlack Linkage)
*
* <p>强制将 SalesDemandQty 与 DemandSlack 关联起来,确保未满足的销售需求
* 必须通过 DemandSlack 变量暴露,从而触发目标函数中的惩罚。</p>
*
* <h3>问题背景</h3>
* <p>在没有此约束时,求解器可以通过降低 SalesDemandQty 来"规避"需求缺口,
* 而不是通过 DemandSlack 来"暴露"需求缺口。因为 DemandSlack 在物料平衡约束中
* 位于流入侧,SalesDemandQty 在流出侧,求解器可以同时降低两者来平衡方程,
* 从而避免触发 DemandSlack 的高额惩罚(权重 100)。</p>
*
* <h3>数学公式</h3>
* <pre>
* 对于每个 PISPIP (Product × StockingPoint × Period):
* Σ SalesDemandQty + DemandSlack >= Σ DemandQuantity
*
* 即: 实际销售量 + 需求缺口 >= 原始需求总量
* 等价于: DemandSlack >= Σ(DemandQuantity - SalesDemandQty)
* </pre>
*
* <h3>效果</h3>
* <p>添加此约束后,当 SalesDemandQty < DemandQuantity 时,
* DemandSlack 必须填补缺口,从而在目标函数中产生惩罚。
* 求解器被激励去增加生产来满足需求,而非简单地降低销售量。</p>
*
* @see BalanceConstraint 物料平衡约束(DemandSlack 在流入侧)
* @see DemandVariableBuilder 需求变量定义(SalesDemandQty, DemandSlack)
*/
public class DemandSlackLinkageConstraint {
/**
* 构建需求缺口联动约束。
*
* <p>对每个 PISPIP 创建一条约束:
* Σ SalesDemandQty + DemandSlack >= Σ DemandQuantity</p>
*
* @param model 模型容器(提供 SalesDemandQty 和 DemandSlack 变量)
* @param data 测试数据(提供 SalesDemand 列表)
*/
public static void build(MacroPlannerModel model, TestDataBuilder data) {
Map<String, MPVariable> sdVars = model.getSalesDemandQtyVars();
Map<String, MPVariable> slackVars = model.getDemandSlackVars();
for (Product prod : data.getProducts()) {
for (StockingPoint sp : data.getStockingPointsForProduct(prod.getId())) {
for (Period p : data.getPeriods()) {
String pispipKey = prod.getId() + "_" + sp.getId() + "_" + p.getIndex();
double totalDemandQty = 0.0;
boolean hasDemand = false;
// 汇总该 PISPIP 的所有销售需求
for (SalesDemand sd : data.getSalesDemandsFor(prod, sp, p)) {
totalDemandQty += sd.getQuantity();
hasDemand = true;
}
// 只对存在销售需求的 PISPIP 创建约束
if (!hasDemand) continue;
// 约束: Σ SalesDemandQty + DemandSlack >= totalDemandQty
MPConstraint c = model.getSolver().makeConstraint(
totalDemandQty, MPSolver.infinity(),
"DSLink_" + pispipKey);
// + Σ SalesDemandQty
for (SalesDemand sd : data.getSalesDemandsFor(prod, sp, p)) {
MPVariable sdVar = sdVars.get(sd.getKey());
if (sdVar != null) c.setCoefficient(sdVar, 1.0);
}
// + DemandSlack
MPVariable slackVar = slackVars.get(pispipKey);
if (slackVar != null) c.setCoefficient(slackVar, 1.0);
}
}
}
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
import java.util.*;
import java.util.logging.Logger;
/**
* 数据完整性检查器 — 在求解前验证数据模型的完整性和一致性。
*
* <h3>检查项</h3>
* <ol>
* <li>产品→库存点映射: 每个产品至少有一个库存点</li>
* <li>产品→工序覆盖: 有需求的产品必须有对应的生产工序</li>
* <li>BOM 一致性: 输入物料的产品/库存点存在, 无循环依赖</li>
* <li>工序→单元映射: 每个工序的 Unit 在 UnitPeriod 中存在</li>
* <li>引用完整性: 所有数据引用的产品/库存点/周期/工序存在</li>
* <li>提前期可行性: leadTime 不超过周期范围</li>
* <li>原材料供应: 被 BOM 消耗但无生产的原材料需有初始库存或在途供应</li>
* <li>孤立产品: 无需求、无生产、无 BOM 角色的产品</li>
* </ol>
*/
public class DataValidator {
private static final Logger LOG = Logger.getLogger(DataValidator.class.getName());
private final TestDataBuilder data;
private final List<String> errors = new ArrayList<>();
private final List<String> warnings = new ArrayList<>();
public DataValidator(TestDataBuilder data) {
this.data = data;
}
/** 执行所有检查, 返回是否有错误 */
public boolean validate() {
errors.clear();
warnings.clear();
checkProductStockingPoint();
checkProductOperationCoverage();
checkBomConsistency();
checkOperationUnitMapping();
checkReferenceIntegrity();
checkLeadTimeFeasibility();
checkRawMaterialSupply();
checkOrphanProducts();
// 输出结果
if (!errors.isEmpty()) {
LOG.severe("========== 数据检查: " + errors.size() + " 个错误 ==========");
for (String e : errors) {
LOG.severe(" ❌ " + e);
}
}
if (!warnings.isEmpty()) {
LOG.warning("========== 数据检查: " + warnings.size() + " 个警告 ==========");
for (String w : warnings) {
LOG.warning(" ⚠️ " + w);
}
}
if (errors.isEmpty() && warnings.isEmpty()) {
LOG.info("✅ 数据检查通过: 无错误, 无警告");
} else if (errors.isEmpty()) {
LOG.info("✅ 数据检查通过: 无错误, " + warnings.size() + " 个警告");
}
return errors.isEmpty();
}
// ==================== 1. 产品→库存点映射 ====================
private void checkProductStockingPoint() {
for (Product prod : data.getProducts()) {
List<StockingPoint> sps = data.getStockingPointsForProduct(prod.getId());
if (sps.isEmpty()) {
errors.add("产品 " + prod.getId() + " 没有指定库存点 (缺少 ProductSpMapping)");
}
}
}
// ==================== 2. 产品→工序覆盖 ====================
private void checkProductOperationCoverage() {
// 收集哪些产品有外部需求
Set<String> productsWithDemand = new HashSet<>();
for (SalesDemand sd : data.getSalesDemands()) {
productsWithDemand.add(sd.getProduct().getId());
}
// 收集哪些产品有 BOM 消耗 (被其他工序作为输入)
Set<String> productsWithBomDemand = new HashSet<>();
for (OperationInput input : data.getOperationInputs()) {
productsWithBomDemand.add(input.getInputProduct().getId());
}
// 收集哪些产品有生产工序
Set<String> productsWithOperation = new HashSet<>();
for (Operation op : data.getOperations()) {
productsWithOperation.add(op.getOutputProductId());
}
// 有销售需求但没有生产工序
for (String prodId : productsWithDemand) {
if (!productsWithOperation.contains(prodId)) {
errors.add("产品 " + prodId + " 有销售需求, 但没有生产工序 (缺少 Operation)");
}
}
// 有 BOM 消耗但没有生产工序 (且不是原材料)
for (String prodId : productsWithBomDemand) {
if (!productsWithOperation.contains(prodId)) {
// 检查是否有初始库存或在途供应
boolean hasSupply = false;
for (InitialInventory inv : data.getInitialInventories()) {
if (inv.getProduct().getId().equals(prodId)) {
hasSupply = true;
break;
}
}
for (InTransitSupply its : data.getInTransitSupplies()) {
if (its.getProduct().getId().equals(prodId)) {
hasSupply = true;
break;
}
}
if (!hasSupply) {
errors.add("产品 " + prodId + " 被 BOM 消耗, 但没有生产工序, 也没有初始库存或在途供应");
} else {
warnings.add("产品 " + prodId + " 被 BOM 消耗但没有生产工序, 依赖初始库存/在途供应 (消耗完后将无法补货)");
}
}
}
}
// ==================== 3. BOM 一致性 ====================
private void checkBomConsistency() {
Set<String> productIds = new HashSet<>();
for (Product p : data.getProducts()) productIds.add(p.getId());
Set<String> spIds = new HashSet<>();
for (StockingPoint sp : data.getStockingPoints()) spIds.add(sp.getId());
Set<String> opIds = new HashSet<>();
for (Operation op : data.getOperations()) opIds.add(op.getId());
for (OperationInput input : data.getOperationInputs()) {
Operation op = input.getOperation();
Product inputProd = input.getInputProduct();
StockingPoint inputSp = input.getInputSp();
// 工序存在
if (!opIds.contains(op.getId())) {
errors.add("BOM 输入引用不存在的工序: " + op.getId());
}
// 投入产品存在
if (!productIds.contains(inputProd.getId())) {
errors.add("BOM 输入引用不存在的产品: " + inputProd.getId()
+ " (工序 " + op.getId() + ")");
}
// 投入库存点存在
if (!spIds.contains(inputSp.getId())) {
errors.add("BOM 输入引用不存在的库存点: " + inputSp.getId()
+ " (工序 " + op.getId() + " 消耗 " + inputProd.getId() + ")");
}
// 工序不能消耗自己产出的产品 (自循环) — 仅当输入和输出在同一库存点时才报错
// 多工序路由中同一产品可经不同库存点流转 (如: 下料→SP_WIP1→粗加工→SP_WIP2→精加工→SP_FG)
if (op.getOutputProductId().equals(inputProd.getId())
&& op.getOutputSpId().equals(inputSp.getId())) {
errors.add("工序 " + op.getId() + " 消耗自己产出的产品 " + inputProd.getId()
+ "@" + inputSp.getId() + " (自循环 BOM, 同一库存点)");
}
// BOM 因子 > 0
if (input.getFactor() <= 0) {
errors.add("BOM 因子必须 > 0: " + op.getId() + " 消耗 " + inputProd.getId()
+ " 因子=" + input.getFactor());
}
}
// 循环依赖检测 (含库存点: 仅当产品+库存点都匹配时才构成循环)
// 多工序路由中同一产品经不同库存点流转不构成循环
for (OperationInput input : data.getOperationInputs()) {
String consumerProd = input.getOperation().getOutputProductId();
String consumerSp = input.getOperation().getOutputSpId();
String consumedProd = input.getInputProduct().getId();
String consumedSp = input.getInputSp().getId();
for (OperationInput other : data.getOperationInputs()) {
// 检查 other 是否产出 consumedProd@consumedSp 且消耗 consumerProd@consumerSp
if (other.getOperation().getOutputProductId().equals(consumedProd)
&& other.getOperation().getOutputSpId().equals(consumedSp)
&& other.getInputProduct().getId().equals(consumerProd)
&& other.getInputSp().getId().equals(consumerSp)) {
errors.add("BOM 循环依赖: " + consumerProd + "@" + consumerSp
+ " → " + consumedProd + "@" + consumedSp
+ " → " + consumerProd + "@" + consumerSp);
}
}
}
}
// ==================== 4. 工序→单元映射 ====================
private void checkOperationUnitMapping() {
Set<String> unitIds = new HashSet<>();
for (UnitPeriod up : data.getUnitPeriods()) {
unitIds.add(up.getUnitId());
}
for (Operation op : data.getOperations()) {
if (!unitIds.contains(op.getUnitId())) {
errors.add("工序 " + op.getId() + " 的单元 " + op.getUnitId()
+ " 在 UnitPeriod 中不存在 (缺少产能定义)");
}
}
}
// ==================== 5. 引用完整性 ====================
private void checkReferenceIntegrity() {
Set<String> productIds = new HashSet<>();
for (Product p : data.getProducts()) productIds.add(p.getId());
Set<String> spIds = new HashSet<>();
for (StockingPoint sp : data.getStockingPoints()) spIds.add(sp.getId());
Set<String> opIds = new HashSet<>();
for (Operation op : data.getOperations()) opIds.add(op.getId());
int maxPeriodIdx = data.getPeriods().size() - 1;
// 销售需求
for (SalesDemand sd : data.getSalesDemands()) {
if (!productIds.contains(sd.getProduct().getId())) {
errors.add("销售需求引用不存在的产品: " + sd.getProduct().getId());
}
if (!spIds.contains(sd.getStockingPoint().getId())) {
errors.add("销售需求引用不存在的库存点: " + sd.getStockingPoint().getId());
}
if (sd.getPeriod().getIndex() > maxPeriodIdx) {
errors.add("销售需求引用不存在的周期: " + sd.getPeriod().getIndex());
}
if (sd.getQuantity() < 0) {
errors.add("销售需求数量不能为负: " + sd.getProduct().getId() + " P" + sd.getPeriod().getIndex());
}
}
// 库存规格
for (InventorySpec spec : data.getInventorySpecs()) {
if (!productIds.contains(spec.getProduct().getId())) {
errors.add("库存规格引用不存在的产品: " + spec.getProduct().getId());
}
if (!spIds.contains(spec.getStockingPoint().getId())) {
errors.add("库存规格引用不存在的库存点: " + spec.getStockingPoint().getId());
}
}
// 供应规格
for (SupplySpec spec : data.getSupplySpecs()) {
for (Operation op : spec.getOperations()) {
if (!opIds.contains(op.getId())) {
errors.add("供应规格 " + spec.getName() + " 引用不存在的工序: " + op.getId());
}
}
}
// 初始库存
for (InitialInventory inv : data.getInitialInventories()) {
if (!productIds.contains(inv.getProduct().getId())) {
errors.add("初始库存引用不存在的产品: " + inv.getProduct().getId());
}
if (!spIds.contains(inv.getStockingPoint().getId())) {
errors.add("初始库存引用不存在的库存点: " + inv.getStockingPoint().getId());
}
if (inv.getQuantity() < 0) {
errors.add("初始库存不能为负: " + inv.getProduct().getId()
+ "@" + inv.getStockingPoint().getId());
}
}
// 在途供应
for (InTransitSupply its : data.getInTransitSupplies()) {
if (!productIds.contains(its.getProduct().getId())) {
errors.add("在途供应引用不存在的产品: " + its.getProduct().getId());
}
if (!spIds.contains(its.getStockingPoint().getId())) {
errors.add("在途供应引用不存在的库存点: " + its.getStockingPoint().getId());
}
}
}
// ==================== 6. 提前期可行性 ====================
private void checkLeadTimeFeasibility() {
int numPeriods = data.getPeriods().size();
for (Operation op : data.getOperations()) {
int leadTimeDays = op.getLeadTimeDays();
if (leadTimeDays < 0) {
errors.add("工序 " + op.getId() + " leadTimeDays 不能为负: " + leadTimeDays);
}
// 检查是否有 startDate 做日期偏移
Period firstPeriod = data.getPeriods().get(0);
if (firstPeriod.getStartDate() != null && leadTimeDays > 0) {
Period srcPeriod = data.getPeriodOffsetByDays(firstPeriod, leadTimeDays);
if (srcPeriod == null) {
warnings.add("工序 " + op.getId() + " leadTimeDays=" + leadTimeDays
+ " 天, 但第一个周期前无对应生产周期, 第1周期该产品无到货");
}
}
}
}
// ==================== 7. 原材料供应 ====================
private void checkRawMaterialSupply() {
// 收集所有产品
Set<String> productIds = new HashSet<>();
for (Product p : data.getProducts()) productIds.add(p.getId());
// 有生产工序的产品
Set<String> productsWithOperation = new HashSet<>();
for (Operation op : data.getOperations()) {
productsWithOperation.add(op.getOutputProductId());
}
// 被 BOM 消耗的产品
Set<String> productsConsumed = new HashSet<>();
for (OperationInput input : data.getOperationInputs()) {
productsConsumed.add(input.getInputProduct().getId());
}
// 有初始库存的产品
Set<String> productsWithInitInv = new HashSet<>();
for (InitialInventory inv : data.getInitialInventories()) {
if (inv.getQuantity() > 0) {
productsWithInitInv.add(inv.getProduct().getId());
}
}
// 有在途供应的产品
Set<String> productsWithInTransit = new HashSet<>();
for (InTransitSupply its : data.getInTransitSupplies()) {
if (its.getQuantity() > 0) {
productsWithInTransit.add(its.getProduct().getId());
}
}
// 被消耗但没有生产 = 纯原材料, 检查是否有初始库存或在途
for (String prodId : productsConsumed) {
if (!productsWithOperation.contains(prodId)) {
// 是纯原材料
if (!productsWithInitInv.contains(prodId) && !productsWithInTransit.contains(prodId)) {
warnings.add("原材料 " + prodId + " 被 BOM 消耗但没有初始库存和在途供应, "
+ "第1周期可能无法满足消耗需求");
}
}
}
// 有销售需求的产品
Set<String> productsWithSalesDemand = new HashSet<>();
for (SalesDemand sd : data.getSalesDemands()) {
productsWithSalesDemand.add(sd.getProduct().getId());
}
for (String prodId : productsWithSalesDemand) {
if (!productsWithOperation.contains(prodId)) {
errors.add("产品 " + prodId + " 有销售需求但没有生产工序");
}
}
}
// ==================== 8. 孤立产品 ====================
private void checkOrphanProducts() {
// 有销售需求的产品
Set<String> productsWithDemand = new HashSet<>();
for (SalesDemand sd : data.getSalesDemands()) {
productsWithDemand.add(sd.getProduct().getId());
}
// 有生产工序的产品
Set<String> productsWithOperation = new HashSet<>();
for (Operation op : data.getOperations()) {
productsWithOperation.add(op.getOutputProductId());
}
// 在 BOM 中作为输入的产品
Set<String> productsInBom = new HashSet<>();
for (OperationInput input : data.getOperationInputs()) {
productsInBom.add(input.getInputProduct().getId());
}
for (Product prod : data.getProducts()) {
String pid = prod.getId();
boolean hasDemand = productsWithDemand.contains(pid);
boolean hasOperation = productsWithOperation.contains(pid);
boolean inBom = productsInBom.contains(pid);
// 既无需求、无生产、也不在 BOM 中 → 完全孤立
if (!hasDemand && !hasOperation && !inBom) {
warnings.add("产品 " + pid + " 是孤立产品: 无销售需求, 无生产工序, 无 BOM 角色");
}
}
}
// ==================== 访问检查结果 ====================
public List<String> getErrors() { return Collections.unmodifiableList(errors); }
public List<String> getWarnings() { return Collections.unmodifiableList(warnings); }
public boolean hasErrors() { return !errors.isEmpty(); }
public boolean hasWarnings() { return !warnings.isEmpty(); }
}
\ No newline at end of file
package com.aps.macroplanner.data;
import java.time.LocalDate;
/**
* 在途供应 (InTransitSupply) — 原材料供应商已发货、将在未来到货的固定供应量。
*
* <p>与初始库存不同,在途供应是计划在未来某个日期到达的固定流入量,
* 不是决策变量,而是作为物料平衡约束中的已知常量。</p>
*
* <p>典型场景: 供应商已承诺在 2026-01-07 交付 100 件原材料 R1</p>
*
* <p>arrivalDate 通过 TestDataBuilder.getPeriodByDate() 映射到对应的周期。</p>
*/
public class InTransitSupply {
private final Product product;
private final StockingPoint stockingPoint;
private final LocalDate arrivalDate;
private final double quantity;
public InTransitSupply(Product product, StockingPoint stockingPoint,
LocalDate arrivalDate, double quantity) {
this.product = product;
this.stockingPoint = stockingPoint;
this.arrivalDate = arrivalDate;
this.quantity = quantity;
}
public Product getProduct() { return product; }
public StockingPoint getStockingPoint() { return stockingPoint; }
public LocalDate getArrivalDate() { return arrivalDate; }
public double getQuantity() { return quantity; }
@Override
public String toString() {
return product.getId() + "@" + stockingPoint.getId()
+ " 到货" + arrivalDate + " " + quantity + "件";
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
/**
* 初始库存 — 定义某个产品在某个库存点的期初库存量。
*
* <p>替代之前用字符串 key ("productId_spId") 的 Map 方式, 更清晰。</p>
*/
public class InitialInventory {
private final Product product;
private final StockingPoint stockingPoint;
private final double quantity;
public InitialInventory(Product product, StockingPoint stockingPoint, double quantity) {
this.product = product;
this.stockingPoint = stockingPoint;
this.quantity = quantity;
}
public Product getProduct() { return product; }
public StockingPoint getStockingPoint() { return stockingPoint; }
public double getQuantity() { return quantity; }
@Override
public String toString() {
return product.getId() + "@" + stockingPoint.getId() + " = " + quantity;
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
/**
* 操作输出 (OperationOutput) — 对应 Quintiq 中的 OperationOutput → PISP
*
* <p>定义某个操作 (Operation) 生产的产品及其产出到的库存点。
* 与 {@link OperationInput} 对称: 输入用 Product + StockingPoint, 输出也用 Product + StockingPoint。</p>
*
* <h3>多工序路由场景</h3>
* <pre>
* 下料 → OperationOutput(P, SP_WIP1)
* 粗加工 → OperationOutput(P, SP_WIP2)
* 精加工 → OperationOutput(P, SP_FG)
* </pre>
* 同一产品经不同工序产出到不同库存点, 通过 {@link OperationOutput} 的 stockingPoint 区分。
*/
public class OperationOutput {
private final Product product; // 产出产品
private final StockingPoint stockingPoint; // 产出到哪个库存点
public OperationOutput(Product product, StockingPoint stockingPoint) {
this.product = product;
this.stockingPoint = stockingPoint;
}
public Product getProduct() { return product; }
public StockingPoint getStockingPoint() { return stockingPoint; }
/** 便捷方法: 产品 ID */
public String getProductId() { return product.getId(); }
/** 便捷方法: 库存点 ID */
public String getSpId() { return stockingPoint.getId(); }
@Override
public String toString() {
return product.getId() + "@" + stockingPoint.getId();
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
/**
* 产品→库存点映射 — 定义一个产品存放在哪个库存点。
*
* <p>替代之前用 Map<String, List<StockingPoint>> 的 productSpMap 方式,
* 声明式地定义产品与库存点的关系。</p>
*
* <p>一个产品可以在多个库存点存放 (通过多个 ProductSpMapping 记录)。</p>
*/
public class ProductSpMapping {
private final Product product;
private final StockingPoint stockingPoint;
public ProductSpMapping(Product product, StockingPoint stockingPoint) {
this.product = product;
this.stockingPoint = stockingPoint;
}
public Product getProduct() { return product; }
public StockingPoint getStockingPoint() { return stockingPoint; }
@Override
public String toString() {
return product.getId() + " → " + stockingPoint.getId();
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Collections;
/**
* 多工序路由测试数据构建器
*
* <h3>测试场景: 3 工序串行生产同一产品 P</h3>
* <pre>
* OP_Cut(下料) ──→ P@SP_WIP1 ──→ OP_Rough(粗加工) ──→ P@SP_WIP2 ──→ OP_Finish(精加工) ──→ P@SP_FG ──→ 销售
* Unit_Cut ↑ Unit_Rough ↑ Unit_Finish ↑
* OP_Rough 消耗 OP_Finish 消耗 销售需求 50/周期
* P@SP_WIP1 (BOM) P@SP_WIP2 (BOM)
*
* 关键验证:
* - 3 个工序各自消耗各自设备的产能 (独立计算)
* - 只有最后一道工序 OP_Finish 的产出计入成品供应
* - 中间工序的产出被 DependentDemand 抵消, 最终产出 = 销售需求, 不是 3×
* </pre>
*/
public class RoutingTestDataBuilder extends TestDataBuilder {
public RoutingTestDataBuilder() {
super(true); // 跳过父类默认 build
build();
}
@Override
protected void build() {
LocalDate baseDate = LocalDate.of(2026, 1, 5);
// === 3 个周期 (每天一个周期) ===
Period p1 = new Period(0, "P1", baseDate);
Period p2 = new Period(1, "P2", baseDate.plusDays(1));
Period p3 = new Period(2, "P3", baseDate.plusDays(2));
periods.addAll(Arrays.asList(p1, p2, p3));
// === 产品: 只有 1 个成品 P ===
Product prodP = new Product("P", "Product-P");
products.add(prodP);
// === 库存点: 2 个 WIP 缓冲区 + 1 个成品库 ===
StockingPoint spWIP1 = new StockingPoint("SP_WIP1", "下料→粗加工缓冲区");
StockingPoint spWIP2 = new StockingPoint("SP_WIP2", "粗加工→精加工缓冲区");
StockingPoint spFG = new StockingPoint("SP_FG", "成品库");
stockingPoints.addAll(Arrays.asList(spWIP1, spWIP2, spFG));
// === 产品→库存点映射 (P 在三个库存点都有) ===
productSpMappings.add(new ProductSpMapping(prodP, spWIP1));
productSpMappings.add(new ProductSpMapping(prodP, spWIP2));
productSpMappings.add(new ProductSpMapping(prodP, spFG));
// === 3 道工序, 都产出产品 P, 但分属不同设备 ===
// 下料: 1.0h/件, 设备 Unit_Cut, 产出到 WIP1 缓冲区
Operation opCut = new Operation("OP_Cut", "下料", "Unit_Cut",
new OperationOutput(prodP, spWIP1), 1.0, 1.0, false, 0, 1.0);
// 粗加工: 1.5h/件, 设备 Unit_Rough, 产出到 WIP2 缓冲区
Operation opRough = new Operation("OP_Rough", "粗加工", "Unit_Rough",
new OperationOutput(prodP, spWIP2), 1.5, 1.0, false, 0, 1.0);
// 精加工: 2.0h/件, 设备 Unit_Finish, 产出到成品库
Operation opFinish = new Operation("OP_Finish", "精加工", "Unit_Finish",
new OperationOutput(prodP, spFG), 2.0, 1.0, false, 0, 1.0);
operations.addAll(Arrays.asList(opCut, opRough, opFinish));
// === BOM / OperationInput: 定义工序间流转 ===
// OP_Rough 消耗 P@SP_WIP1 (即 OP_Cut 的产出), factor=1.0 (1:1 无损耗)
operationInputs.add(new OperationInput(opRough, prodP, spWIP1, 1.0));
// OP_Finish 消耗 P@SP_WIP2 (即 OP_Rough 的产出), factor=1.0
operationInputs.add(new OperationInput(opFinish, prodP, spWIP2, 1.0));
// === 设备产能: 三个设备各有独立产能 ===
// Unit_Cut: 最大 200h/周期
for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_Cut", p, 0.0, 200.0, false));
}
// Unit_Rough: 最大 200h/周期
for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_Rough", p, 0.0, 200.0, false));
}
// Unit_Finish: 最大 200h/周期
for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_Finish", p, 0.0, 200.0, false));
}
// === 初始库存: 全部为 0 (没有初始 WIP) ===
initialInventories.add(new InitialInventory(prodP, spWIP1, 0.0));
initialInventories.add(new InitialInventory(prodP, spWIP2, 0.0));
initialInventories.add(new InitialInventory(prodP, spFG, 0.0));
// === 销售需求: 只在成品库 SP_FG, 每周期 50 ===
for (Period p : periods) {
salesDemands.add(new SalesDemand(prodP, spFG, p, 50.0, 1.0));
}
// === 库存规格: 只在成品库设目标/最小/最大 ===
// WIP 缓冲区不设库存规格 (不约束中间库存)
for (Period p : periods) {
inventorySpecs.add(new InventorySpec(prodP, spFG, p,
80.0, 10.0, 200.0, true, true, true));
}
// === 供应规格: 只统计精加工(最后一道工序)的产出 ===
supplySpecs.add(new SupplySpec("Supply-P", 150.0, 100.0, 300.0,
true, Collections.singletonList(opFinish)));
// === KPI 权重 ===
kpiWeights = new KPIWeights(
100.0, // fulfillmentWeight
10.0, // lotSizeWeight
5.0, // maxInventoryLevelWeight
5.0, // minInventoryLevelWeight
8.0, // targetInventoryLevelWeight
3.0, // unitCapacityWeight
8.0, // supplyTargetWeight
5.0, // minSupplyWeight
5.0, // maxSupplyWeight
1.0, // salesDemandPriorityWeight
20.0, // postponementPenaltyWeight
5.0 // processMaxQuantityWeight
);
}
}
\ No newline at end of file
package com.aps.macroplanner.objective;
import com.google.ortools.linearsolver.MPVariable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 策略层级配置 — 对应 Quintiq 中的 StrategyLevel
*
* <p>每个层级包含一组 KPI,在分层优化中作为一个整体求解。
* 层级越低(level 值越小),优先级越高,越先求解。</p>
*
* <h3>分层优化流程</h3>
* <pre>
* 第1轮: 只优化 Level 1 的 KPI → 记录最优值 V1
* 第2轮: 约束 Level1 ≤ V1×(1+slack) → 优化 Level 2 的 KPI
* 第3轮: 约束 Level1+2 ≤ 最优×(1+slack) → 优化 Level 3 的 KPI
* ...
* </pre>
*
* <h3>默认层级划分</h3>
* <pre>
* Level 1 (需求满足): Fulfillment (需求缺口 = 硬约束)
* Level 2 (产能): UnitCapacity (产能超载 = 物理约束)
* Level 3 (业务KPI): LotSize, TargetInventory, SupplyTarget, SalesPriority
* Level 4 (软约束): MaxInventory, MinInventory, MinSupply, MaxSupply
* </pre>
*
* @see ObjectiveBuilder
* @see com.aps.macroplanner.MacroPlannerOptimizer
*/
public class StrategyLevel {
/** 层级编号 (1 最高优先, 数值越大优先级越低) */
private final int level;
/** 层级名称 (用于日志输出) */
private final String name;
/**
* 目标松弛比例 — 允许上层最优值退化的比例。
* 例如 0.0 表示不允许退化(严格分层),
* 0.05 表示允许上层目标值恶化 5% 以换取下层优化空间。
* 对应 Quintiq 中的 RelativeGoalSlack。
*/
private final double relativeGoalSlack;
/** 该层级包含的 KPI 条目列表 */
private final List<KPIEntry> kpis = new ArrayList<>();
/**
* 单个 KPI 条目 — 封装变量、权重和方向。
*/
public static class KPIEntry {
/** 该 KPI 的汇总变量 (如 TotalFulfillment) */
public final MPVariable variable;
/** 该 KPI 在当前层级内的权重 */
public final double weight;
/** 是否为负向 KPI (越小越好 = 正常惩罚项; false = 正常惩罚项) */
public final boolean isNegative;
/** KPI 名称 (用于日志) */
public final String name;
public KPIEntry(String name, MPVariable variable, double weight, boolean isNegative) {
this.name = name;
this.variable = variable;
this.weight = weight;
this.isNegative = isNegative;
}
/**
* 计算该 KPI 在目标函数中的实际系数。
* 负向 KPI (如 SalesDemandPriority) 使用负系数实现最大化。
*/
public double effectiveCoefficient() {
return isNegative ? -weight : weight;
}
}
/**
* 创建策略层级。
*
* @param level 层级编号 (1-based, 越小优先级越高)
* @param name 层级名称
* @param relativeGoalSlack 目标松弛比例 (0.0 = 严格分层)
*/
public StrategyLevel(int level, String name, double relativeGoalSlack) {
this.level = level;
this.name = name;
this.relativeGoalSlack = relativeGoalSlack;
}
/**
* 添加一个 KPI 到该层级。
*
* @param name KPI 名称
* @param variable KPI 汇总变量
* @param weight 权重 (0 = 跳过)
*/
public void addKPI(String name, MPVariable variable, double weight) {
addKPI(name, variable, weight, false);
}
/**
* 添加一个 KPI 到该层级。
*
* @param name KPI 名称
* @param variable KPI 汇总变量
* @param weight 权重 (0 = 跳过)
* @param isNegative 是否为负向 KPI (true = 最大化, 使用负系数)
*/
public void addKPI(String name, MPVariable variable, double weight, boolean isNegative) {
if (weight > 0.0 && variable != null) {
kpis.add(new KPIEntry(name, variable, weight, isNegative));
}
}
// ==================== Getters ====================
public int getLevel() { return level; }
public String getName() { return name; }
public double getRelativeGoalSlack() { return relativeGoalSlack; }
public List<KPIEntry> getKpis() { return Collections.unmodifiableList(kpis); }
/**
* 该层级是否有任何有效的 KPI。
*/
public boolean hasKpis() {
return !kpis.isEmpty();
}
}
\ No newline at end of file
package com.aps.service.mp;
import com.google.ortools.Loader;
import com.google.ortools.linearsolver.MPConstraint;
import com.google.ortools.linearsolver.MPObjective;
import com.google.ortools.linearsolver.MPSolver;
import com.google.ortools.linearsolver.MPVariable;
/**
* 多阶BOM + MRP 逻辑 MIP 排产
* 三级结构:成品(P1,P2) → 半成品(S1,S2,S3) → 原材料(R1,R2,R3)
* 产线:L1、L2、L3,产品可在指定产线生产(支持多对多映射)
* 特性:BOM自动展开、采购提前期、多级库存平衡、多产线生产、需求缺口(shortfall)
*
* 重构说明:
* - 生产变量从 2D [item][day] 扩展为 3D [item][line][day]
* - 支持产品在多条产线生产(如P1可在L1或L3生产)
* - 通过 canProduce[][] 矩阵控制产品-产线兼容性
* - 同一产品在不同产线可有不同生产效率
* - 引入 shortfall 变量,需求可部分满足,避免产能不足时无解
*/
public class BomMpsScheduling {
// ========== 内部数据类 ==========
/**
* 产品层配置:封装某一层产品的所有参数(支持多产线)
*/
static class LayerConfig {
String[] names; // 产品名称数组
String[] lineNames; // 产线名称数组
double[] prodCost; // 单位生产成本
double[] setupCost; // 换型成本
double[] holdCost; // 库存持有成本
double[] initInv; // 初始库存
double[] lineCapacity; // 各产线日产能(小时)
double[][] prodRateByLine; // 按产线生产效率 [item][line](件/小时)
boolean[][] canProduce; // 产品-产线兼容矩阵 [item][line]
double shortfallPenalty; // 缺口惩罚成本(元/件),仅成品层使用
LayerConfig(String[] names, String[] lineNames,
double[] prodCost, double[] setupCost,
double[] holdCost, double[] initInv,
double[] lineCapacity, double[][] prodRateByLine,
boolean[][] canProduce, double shortfallPenalty) {
this.names = names;
this.lineNames = lineNames;
this.prodCost = prodCost;
this.setupCost = setupCost;
this.holdCost = holdCost;
this.initInv = initInv;
this.lineCapacity = lineCapacity;
this.prodRateByLine = prodRateByLine;
this.canProduce = canProduce;
this.shortfallPenalty = shortfallPenalty;
}
int size() { return names.length; }
int lineCount() { return lineNames.length; }
boolean canProduce(int item, int line) { return canProduce[item][line]; }
}
/**
* 产品层变量:封装某一层的所有决策变量
* 生产变量为三维数组 [item][line][day]
*/
static class LayerVariables {
MPVariable[][][] production; // 产量变量 [item][line][day]
MPVariable[][] inventory; // 库存变量 [item][day]
MPVariable[][][] switchVar; // 生产开关变量 [item][line][day]
MPVariable[][] purchase; // 采购变量 [item][day](仅原材料层用)
MPVariable[][] shortfall; // 需求缺口变量 [item][day](仅成品层用)
}
// ========== 通用方法 ==========
/**
* 创建单层产品的决策变量(支持多产线)
*/
static LayerVariables createLayerVariables(MPSolver solver, LayerConfig config,
int numDays, boolean isPurchaseLayer) {
LayerVariables vars = new LayerVariables();
int n = config.size();
int numLines = config.lineCount();
if (isPurchaseLayer) {
// 原材料层:只有采购量和库存变量
vars.purchase = new MPVariable[n][numDays];
vars.inventory = new MPVariable[n][numDays];
for (int i = 0; i < n; i++) {
for (int t = 0; t < numDays; t++) {
vars.purchase[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
"pr_" + config.names[i] + "_d" + (t + 1));
vars.inventory[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
"ir_" + config.names[i] + "_d" + (t + 1));
}
}
} else {
// 生产层:产量[item][line][day]、库存[item][day]、开关[item][line][day]
vars.production = new MPVariable[n][numLines][numDays];
vars.inventory = new MPVariable[n][numDays];
vars.switchVar = new MPVariable[n][numLines][numDays];
String prefix = config.names[0].startsWith("P") ? "xp" : "xs";
String invPrefix = config.names[0].startsWith("P") ? "ip" : "is";
String swPrefix = config.names[0].startsWith("P") ? "yp" : "ys";
for (int i = 0; i < n; i++) {
for (int t = 0; t < numDays; t++) {
vars.inventory[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
invPrefix + "_" + config.names[i] + "_d" + (t + 1));
}
for (int l = 0; l < numLines; l++) {
if (config.canProduce(i, l)) {
for (int t = 0; t < numDays; t++) {
vars.production[i][l][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
prefix + "_" + config.names[i] + "_" + config.lineNames[l] + "_d" + (t + 1));
vars.switchVar[i][l][t] = solver.makeBoolVar(
swPrefix + "_" + config.names[i] + "_" + config.lineNames[l] + "_d" + (t + 1));
}
}
}
}
// 为成品层创建 shortfall 变量
if (config.shortfallPenalty > 0) {
vars.shortfall = new MPVariable[n][numDays];
for (int i = 0; i < n; i++) {
for (int t = 0; t < numDays; t++) {
vars.shortfall[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
"short_" + config.names[i] + "_d" + (t + 1));
}
}
}
}
return vars;
}
/**
* 添加目标函数系数(生产层:按item-line组合)
*/
static void addProductionObjective(MPObjective obj, LayerVariables vars,
LayerConfig config, int numDays) {
for (int i = 0; i < config.size(); i++) {
for (int l = 0; l < config.lineCount(); l++) {
if (config.canProduce(i, l)) {
for (int t = 0; t < numDays; t++) {
obj.setCoefficient(vars.production[i][l][t], config.prodCost[i]);
obj.setCoefficient(vars.switchVar[i][l][t], config.setupCost[i]);
}
}
}
for (int t = 0; t < numDays; t++) {
obj.setCoefficient(vars.inventory[i][t], config.holdCost[i]);
}
}
// 添加 shortfall 惩罚成本
if (vars.shortfall != null && config.shortfallPenalty > 0) {
for (int i = 0; i < config.size(); i++) {
for (int t = 0; t < numDays; t++) {
obj.setCoefficient(vars.shortfall[i][t], config.shortfallPenalty);
}
}
}
}
/**
* 添加目标函数系数(采购层)
*/
static void addPurchaseObjective(MPObjective obj, LayerVariables vars,
LayerConfig config, int numDays) {
for (int i = 0; i < config.size(); i++) {
for (int t = 0; t < numDays; t++) {
obj.setCoefficient(vars.purchase[i][t], config.prodCost[i]);
obj.setCoefficient(vars.inventory[i][t], config.holdCost[i]);
}
}
}
/**
* 添加库存平衡约束(生产层:本期消耗为外生需求,含shortfall缺口)
* 库存平衡:上期库存 + Σ各产线产量 + shortfall = 本期需求 + 期末库存
*/
static void addInventoryBalanceWithDemand(MPSolver solver, LayerVariables vars,
LayerConfig config, double[][] demand,
int numDays) {
for (int i = 0; i < config.size(); i++) {
for (int t = 0; t < numDays; t++) {
double prevInv = (t == 0) ? config.initInv[i] : 0;
MPConstraint c = solver.makeConstraint(demand[i][t] - prevInv, demand[i][t] - prevInv,
"inv_" + config.names[i] + "_d" + (t + 1));
if (t > 0) c.setCoefficient(vars.inventory[i][t - 1], 1);
// 本期产量 = Σ 各兼容产线产量
for (int l = 0; l < config.lineCount(); l++) {
if (config.canProduce(i, l)) {
c.setCoefficient(vars.production[i][l][t], 1);
}
}
// shortfall 缺口(如果有)
if (vars.shortfall != null) {
c.setCoefficient(vars.shortfall[i][t], 1);
}
c.setCoefficient(vars.inventory[i][t], -1);
}
}
}
/**
* 添加库存平衡约束(半成品层:本期消耗为BOM展开,产量=Σ各产线)
*/
static void addInventoryBalanceWithBom(MPSolver solver, LayerVariables vars,
LayerConfig config, LayerVariables upstreamVars,
double[][] bomMatrix, int numDays) {
for (int s = 0; s < config.size(); s++) {
for (int t = 0; t < numDays; t++) {
double rhs = 0;
MPConstraint c = solver.makeConstraint(0, 0,
"inv_" + config.names[s] + "_d" + (t + 1));
if (t == 0) {
rhs -= config.initInv[s];
} else {
c.setCoefficient(vars.inventory[s][t - 1], 1);
}
// 本期产量 = Σ 各兼容产线产量
for (int l = 0; l < config.lineCount(); l++) {
if (config.canProduce(s, l)) {
c.setCoefficient(vars.production[s][l][t], 1);
}
}
// 本期消耗:Σ 上游产品i的总产量 × bom系数(-)
for (int i = 0; i < bomMatrix[s].length; i++) {
if (bomMatrix[s][i] != 0) {
// 汇总上游产品i在所有产线的产量
for (int l = 0; l < upstreamVars.production[i].length; l++) {
if (upstreamVars.production[i][l][t] != null) {
c.setCoefficient(upstreamVars.production[i][l][t], -bomMatrix[s][i]);
}
}
}
}
c.setCoefficient(vars.inventory[s][t], -1);
c.setBounds(rhs, rhs);
}
}
}
/**
* 添加库存平衡约束(原材料层:BOM + 采购提前期)
*/
static void addInventoryBalanceWithPurchase(MPSolver solver, LayerVariables vars,
LayerConfig config, LayerVariables upstreamVars,
double[][] bomMatrix, int numDays,
int purchaseLeadTime) {
for (int r = 0; r < config.size(); r++) {
for (int t = 0; t < numDays; t++) {
double rhs = 0;
MPConstraint c = solver.makeConstraint(0, 0,
"inv_" + config.names[r] + "_d" + (t + 1));
if (t == 0) {
rhs -= config.initInv[r];
} else {
c.setCoefficient(vars.inventory[r][t - 1], 1);
}
// 本期到货(考虑采购提前期)
int arriveDay = t - purchaseLeadTime;
if (arriveDay >= 0) {
c.setCoefficient(vars.purchase[r][arriveDay], 1);
}
// 本期消耗:Σ 半成品s的总产量 × bom系数
for (int s = 0; s < bomMatrix[r].length; s++) {
if (bomMatrix[r][s] != 0) {
for (int l = 0; l < upstreamVars.production[s].length; l++) {
if (upstreamVars.production[s][l][t] != null) {
c.setCoefficient(upstreamVars.production[s][l][t], -bomMatrix[r][s]);
}
}
}
}
c.setCoefficient(vars.inventory[r][t], -1);
c.setBounds(rhs, rhs);
}
}
}
/**
* 添加产能约束(按产线:该产线所有可生产产品的耗时之和 ≤ 产能)
*/
static void addCapacityConstraint(MPSolver solver, LayerVariables vars,
LayerConfig config, int numDays,
String layerPrefix) {
for (int l = 0; l < config.lineCount(); l++) {
for (int t = 0; t < numDays; t++) {
MPConstraint cap = solver.makeConstraint(0, config.lineCapacity[l],
"cap_" + layerPrefix + "_" + config.lineNames[l] + "_d" + (t + 1));
for (int i = 0; i < config.size(); i++) {
if (config.canProduce(i, l)) {
cap.setCoefficient(vars.production[i][l][t],
1.0 / config.prodRateByLine[i][l]);
}
}
}
}
}
/**
* 添加生产开关约束(产量 ≤ 开关 × bigM,按item-line组合)
*/
static void addProductionSwitchConstraint(MPSolver solver, LayerVariables vars,
LayerConfig config, int numDays,
double bigM, String layerPrefix) {
for (int i = 0; i < config.size(); i++) {
for (int l = 0; l < config.lineCount(); l++) {
if (config.canProduce(i, l)) {
for (int t = 0; t < numDays; t++) {
MPConstraint c = solver.makeConstraint(-Double.POSITIVE_INFINITY, 0,
"switch_" + layerPrefix + "_" + config.names[i] + "_" + config.lineNames[l] + "_d" + (t + 1));
c.setCoefficient(vars.production[i][l][t], 1);
c.setCoefficient(vars.switchVar[i][l][t], -bigM);
}
}
}
}
}
/**
* 计算某产品的总产量(跨产线汇总)
*/
static double getTotalProduction(LayerVariables vars, int item, int day, LayerConfig config) {
double total = 0;
for (int l = 0; l < config.lineCount(); l++) {
if (config.canProduce(item, l)) {
total += vars.production[item][l][day].solutionValue();
}
}
return total;
}
/**
* 打印生产层结果(按产线分组)
*/
static void printProductionResult(LayerVariables vars, LayerConfig config,
int dayIndex) {
String layerLabel = config.names[0].startsWith("P") ? "成品装配" : "半成品加工";
System.out.println(" ▶ " + layerLabel);
for (int l = 0; l < config.lineCount(); l++) {
boolean hasProduction = false;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < config.size(); i++) {
if (config.canProduce(i, l)) {
double qty = vars.production[i][l][dayIndex].solutionValue();
if (qty > 0.001) {
hasProduction = true;
sb.append(String.format("|--> %s: %.0f件 (耗时%.1fh,换型%.0f元)%n",
config.names[i], qty, qty / config.prodRateByLine[i][l],
vars.switchVar[i][l][dayIndex].solutionValue() * config.setupCost[i]));
}
}
}
if (hasProduction) {
System.out.println(" 【" + config.lineNames[l] + "】");
System.out.print(sb);
}
}
// 打印 shortfall 信息(成品层)
if (vars.shortfall != null) {
for (int i = 0; i < config.size(); i++) {
double sht = vars.shortfall[i][dayIndex].solutionValue();
if (sht > 0.001) {
System.out.printf(" ⚠ %s 未满足需求(shortfall):%.0f 件%n",
config.names[i], sht);
}
}
}
}
/**
* 打印采购到货结果
*/
static void printPurchaseResult(LayerVariables vars, LayerConfig config,
int arriveDay) {
System.out.println(" ▶ 原材料到货(第" + (arriveDay + 1) + "天下单)");
for (int r = 0; r < config.size(); r++) {
double qty = vars.purchase[r][arriveDay].solutionValue();
if (qty > 0.001) {
System.out.printf(" %s:到货 %.0f 件(采购成本 %.0f元)%n",
config.names[r], qty, qty * config.prodCost[r]);
}
}
}
/**
* 打印库存结果
*/
static void printInventoryResult(String label, LayerVariables vars,
LayerConfig config, int dayIndex) {
System.out.print(" " + label + ":");
for (int i = 0; i < config.size(); i++) {
System.out.printf("%s=%.0f ", config.names[i],
vars.inventory[i][dayIndex].solutionValue());
}
System.out.println();
}
/**
* 计算层成本明细
* 返回: double[]{生产总成本, 库存持有成本, 换型成本, shortfall成本}
*/
static double[] calcProductionCost(LayerVariables vars, LayerConfig config, int numDays) {
double prodCost = 0, holdCost = 0, setupCost = 0, shortfallCost = 0;
for (int i = 0; i < config.size(); i++) {
for (int l = 0; l < config.lineCount(); l++) {
if (config.canProduce(i, l)) {
for (int t = 0; t < numDays; t++) {
prodCost += vars.production[i][l][t].solutionValue() * config.prodCost[i];
setupCost += vars.switchVar[i][l][t].solutionValue() * config.setupCost[i];
}
}
}
for (int t = 0; t < numDays; t++) {
holdCost += vars.inventory[i][t].solutionValue() * config.holdCost[i];
}
}
// shortfall 成本
if (vars.shortfall != null) {
for (int i = 0; i < config.size(); i++) {
for (int t = 0; t < numDays; t++) {
shortfallCost += vars.shortfall[i][t].solutionValue() * config.shortfallPenalty;
}
}
}
return new double[]{prodCost, holdCost, setupCost, shortfallCost};
}
/**
* 计算采购层成本
* 返回: double[]{采购成本, 库存持有成本}
*/
static double[] calcPurchaseCost(LayerVariables vars, LayerConfig config, int numDays) {
double purchaseCost = 0, holdCost = 0;
for (int i = 0; i < config.size(); i++) {
for (int t = 0; t < numDays; t++) {
purchaseCost += vars.purchase[i][t].solutionValue() * config.prodCost[i];
holdCost += vars.inventory[i][t].solutionValue() * config.holdCost[i];
}
}
return new double[]{purchaseCost, holdCost};
}
// ========== 主方法 ==========
public static void main(String[] args) {
Loader.loadNativeLibraries();
// ========== 1. 维度定义 ==========
int numDays = 3;
int purchaseLeadTime = 1;
double bigM = 10000;
// ========== 2. 产线定义 ==========
String[] lineNames = {"L1", "L2", "L3"};
int numLines = lineNames.length;
double[] lineCapacity = {10, 10, 8}; // 每条产线日产能(小时)
// ========== 3. 产品层配置 ==========
// 成品(可在L1或L3生产)
String[] productNames = {"P1", "P2"};
int numProducts = productNames.length;
boolean[][] productCanProduce = {
{true, false, true}, // P1: L1、L3 可生产
{true, false, true} // P2: L1、L3 可生产
};
double[][] productProdRateByLine = {
{10, 0, 8}, // P1在L1效率10件/小时,L3为8
{8, 0, 6} // P2在L1效率8件/小时,L3为6
};
// shortfall 惩罚成本(高价值产品,优先满足需求)
double shortfallPenalty = 1000; // 每缺1件罚1000元
LayerConfig productConfig = new LayerConfig(
productNames, lineNames,
new double[]{10, 15}, // prodCost
new double[]{200, 300}, // setupCost
new double[]{1.0, 1.5}, // holdCost
new double[]{20, 10}, // initInv
lineCapacity,
productProdRateByLine,
productCanProduce,
shortfallPenalty // 有缺口惩罚
);
// 半成品(可在L2或L3生产)
String[] semiNames = {"S1", "S2", "S3"};
int numSemi = semiNames.length;
boolean[][] semiCanProduce = {
{false, true, true}, // S1: L2、L3
{false, true, true}, // S2: L2、L3
{false, true, true} // S3: L2、L3
};
double[][] semiProdRateByLine = {
{0, 30, 25}, // S1
{0, 25, 20}, // S2
{0, 40, 30} // S3
};
LayerConfig semiConfig = new LayerConfig(
semiNames, lineNames,
new double[]{3, 4, 2},
new double[]{80, 100, 60},
new double[]{0.3, 0.4, 0.2},
new double[]{50, 30, 40},
lineCapacity,
semiProdRateByLine,
semiCanProduce,
0 // 半成品无 shortfall(由成品需求推导)
);
// 原材料(外购,无生产)
String[] materialNames = {"R1", "R2", "R3"};
int numMaterials = materialNames.length;
boolean[][] materialCanProduce = {
{false, false, false},
{false, false, false},
{false, false, false}
};
LayerConfig materialConfig = new LayerConfig(
materialNames, lineNames,
new double[]{1, 1.5, 2},
null, // setupCost 不适用
new double[]{0.1, 0.15, 0.2},
new double[]{200, 100, 80},
lineCapacity,
null, // prodRateByLine 不适用
materialCanProduce,
0 // 原材料无 shortfall
);
// ========== 4. BOM 结构 ==========
// bomProduct[s][i]: 1件成品i 需要半成品s 的数量
double[][] bomProduct = {
{2, 0}, // S1
{0, 1}, // S2
{1, 2} // S3
};
// bomMaterial[r][s]: 1件半成品s 需要原料r 的数量
double[][] bomMaterial = {
{3, 0, 1}, // R1
{0, 2, 0}, // R2
{0, 0, 1} // R3
};
// ========== 5. 成品需求 ==========
double[][] demand = {
{50, 60, 40}, // P1
{30, 40, 50} // P2
};
// ========== 6. 创建求解器 ==========
MPSolver solver = new MPSolver("demo",
MPSolver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING);
solver.enableOutput();
// ========== 7. 创建决策变量 ==========
LayerVariables productVars = createLayerVariables(solver, productConfig, numDays, false);
LayerVariables semiVars = createLayerVariables(solver, semiConfig, numDays, false);
LayerVariables materialVars = createLayerVariables(solver, materialConfig, numDays, true);
// ========== 8. 目标函数 ==========
MPObjective obj = solver.objective();
addProductionObjective(obj, productVars, productConfig, numDays);
addProductionObjective(obj, semiVars, semiConfig, numDays);
addPurchaseObjective(obj, materialVars, materialConfig, numDays);
obj.setMinimization();
// ========== 9. 约束条件 ==========
// 9.1 成品层:库存平衡(外生需求 + shortfall)
addInventoryBalanceWithDemand(solver, productVars, productConfig, demand, numDays);
// 9.2 半成品层:库存平衡(BOM展开)
addInventoryBalanceWithBom(solver, semiVars, semiConfig,
productVars, bomProduct, numDays);
// 9.3 原材料层:库存平衡(BOM + 采购提前期)
addInventoryBalanceWithPurchase(solver, materialVars, materialConfig,
semiVars, bomMaterial, numDays, purchaseLeadTime);
// 9.4 产能约束(按产线)
addCapacityConstraint(solver, productVars, productConfig, numDays, "prod");
addCapacityConstraint(solver, semiVars, semiConfig, numDays, "semi");
// 9.5 生产开关约束
addProductionSwitchConstraint(solver, productVars, productConfig, numDays, bigM, "prod");
addProductionSwitchConstraint(solver, semiVars, semiConfig, numDays, bigM, "semi");
// ========== 10. 求解 ==========
System.out.println("========== 多阶BOM + MRP 排产求解 ==========");
System.out.printf("成品%d种,半成品%d种,原料%d种,产线%d条,周期%d天%n",
numProducts, numSemi, numMaterials, numLines, numDays);
System.out.printf("产线:%s,成品可生产:P1(L1,L3) P2(L1,L3),半成品可生产:S1-S3(L2,L3)%n",
java.util.Arrays.toString(lineNames));
System.out.printf("需求缺口惩罚:%.0f 元/件%n%n", shortfallPenalty);
MPSolver.ResultStatus status = solver.solve();
// ========== 11. 结果输出 ==========
if (status == MPSolver.ResultStatus.OPTIMAL) {
System.out.println("✅ 求解成功!全局最优解");
System.out.printf("最小总成本:%.2f 元%n%n", obj.value());
boolean hasShortfall = false;
for (int t = 0; t < numDays; t++) {
System.out.println("═══════════════════ 第 " + (t + 1) + " 天 ═══════════════════");
// 成品生产
printProductionResult(productVars, productConfig, t);
// 半成品生产
printProductionResult(semiVars, semiConfig, t);
// 原材料采购到货
int arriveDay = t - purchaseLeadTime;
if (arriveDay >= 0) {
printPurchaseResult(materialVars, materialConfig, arriveDay);
}
// 期末库存
System.out.println(" ▶ 期末库存");
printInventoryResult("成品", productVars, productConfig, t);
printInventoryResult("半成", semiVars, semiConfig, t);
printInventoryResult("原料", materialVars, materialConfig, t);
System.out.println();
}
// ========== 12. 成本明细 ==========
double[] costP = calcProductionCost(productVars, productConfig, numDays);
double[] costS = calcProductionCost(semiVars, semiConfig, numDays);
double[] costM = calcPurchaseCost(materialVars, materialConfig, numDays);
System.out.println("═══════════════════ 成本明细 ═══════════════════");
System.out.printf("成品生产:%8.2f (换型 %.0f + 物料加工 %.0f)%n",
costP[0] + costP[2], costP[2], costP[0]);
System.out.printf("半品生产:%8.2f (换型 %.0f + 物料加工 %.0f)%n",
costS[0] + costS[2], costS[2], costS[0]);
System.out.printf("原料采购:%8.2f%n", costM[0]);
System.out.printf("库存持有:%8.2f (成品%.1f + 半成%.1f + 原料%.1f)%n",
costP[1] + costS[1] + costM[1], costP[1], costS[1], costM[1]);
if (costP[3] > 0.001) {
System.out.printf("⚠ 需求缺口:%8.2f 元%n", costP[3]);
hasShortfall = true;
}
System.out.printf("──────────────────────────────%n");
System.out.printf("总 成 本:%8.2f 元%n", obj.value());
if (hasShortfall) {
System.out.println();
System.out.println("⚠ 注意:存在需求缺口,部分订单未满足。请检查产能或增加产能投入。");
}
System.out.println();
System.out.println("═══════════════════ 求解统计 ═══════════════════");
System.out.println("变量数:" + solver.numVariables());
System.out.println("约束数:" + solver.numConstraints());
System.out.printf("耗时:%.3f 秒%n", solver.wallTime() / 1000.0);
} else if (status == MPSolver.ResultStatus.INFEASIBLE) {
System.out.println("❌ 无解 —— 请检查产能、库存或需求约束是否合理");
} else {
System.out.println("求解状态:" + status);
}
}
}
\ No newline at end of file
package com.aps.service.mp;
import com.google.ortools.Loader;
import com.google.ortools.linearsolver.MPConstraint;
import com.google.ortools.linearsolver.MPObjective;
import com.google.ortools.linearsolver.MPSolver;
import com.google.ortools.linearsolver.MPVariable;
import java.util.*;
/**
* 多阶BOM + MRP 逻辑 MIP 排产(通用网状BOM架构)
*
* 架构说明:
* - 基于节点的网状BOM结构,支持任意层级深度
* - 产品可直接消耗半成品和原材料(混合BOM)
* - 每个产品节点可同时具备:生产、采购、外部需求、BOM子项
*
* 核心公式(库存平衡):
* 上期库存 + Σ(各产线产量) + 采购到货 + shortfall
* = 外部需求 + Σ(父节点产量 × BOM系数) + 期末库存
*
* 节点类型:
* - 成品(hasExternalDemand=true):有外部需求 + shortfall
* - 中间品(hasExternalDemand=false):由父节点需求推导
* - 采购品(isPurchased=true):用采购变量而非生产变量
*/
public class BomMpsScheduling1 {
// ========== 内部数据类 ==========
/**
* BOM子项:父节点消耗子节点的记录
*/
static class BomChild {
ProductNode child; // 子节点引用
double coefficient; // 消耗系数(1件父产品消耗多少件子产品)
BomChild(ProductNode child, double coefficient) {
this.child = child;
this.coefficient = coefficient;
}
}
/**
* BOM父引用:子节点被哪个父节点消耗
*/
static class BomParentRef {
ProductNode parent; // 父节点引用
double coefficient; // 消耗系数
BomParentRef(ProductNode parent, double coefficient) {
this.parent = parent;
this.coefficient = coefficient;
}
}
/**
* 产品节点:通用产品定义
*
* 变量创建规则:
* - 生产型:production[line][day], switchVar[line][day]
* - 采购型:purchase[day]
* - 所有节点:inventory[day]
* - 有外部需求:shortfall[day]
*/
static class ProductNode {
String name;
double prodCost; // 单位生产成本(或采购成本)
double setupCost; // 换型成本
double holdCost; // 库存持有成本
double initInv; // 初始库存
double safetyStock; // 安全库存(软约束,低于此值有惩罚)
double minStock; // 最小库存(硬约束,不得低于此值)
double maxStock; // 最大库存(硬约束,不得高于此值)
double overstockPenalty; // 超库存惩罚成本(超过maxStock时惩罚)
double shortfallPenalty; // 缺口惩罚(仅外部需求节点)
double[] prodRateByLine; // 按产线生产效率 [line](件/小时)
boolean[] canProduceOnLine; // 产线兼容性
double[] lineCapacity; // 产线日产能
String[] lineNames; // 产线名称
// 生产批量配置
double minLotSize; // 最小生产批量
double lotMultiple; // 批量步长(Lot Multiple),0表示不启用
boolean enableLotMultiple; // 是否启用批量步长约束
boolean isPurchased; // 是否为外购品
boolean hasExternalDemand; // 是否有外部需求
double[] demand; // 外部需求 [day](仅 hasExternalDemand=true)
// 多供应商配置(仅采购品使用)
List<Supplier> suppliers = new ArrayList<>();
List<BomChild> bomChildren = new ArrayList<>(); // 我消耗谁
List<BomParentRef> bomParents = new ArrayList<>(); // 谁消耗我
// 求解变量
MPVariable[][] inventory;
MPVariable[][] production;
MPVariable[][] switchVar;
MPVariable[][][] purchase; // [supplier][0][day] 多供应商采购
MPVariable[][] purchaseSwitch; // [supplier][day] 采购开关变量(用于最小采购批量)
MPVariable[][][] lotMultipleVar; // [line][day] 批量步长整数变量
MPVariable[][][] supplierLotMultipleVar; // [supplier][0][day] 供应商批量步长整数变量
MPVariable[][] shortfall;
MPVariable[][] underSafety; // 低于安全库存的量(惩罚用)
MPVariable[][] overMaxStock; // 超过最大库存的量(惩罚用)
boolean variablesCreated = false;
ProductNode(String name) {
this.name = name;
}
int lineCount() { return lineNames != null ? lineNames.length : 0; }
boolean canProduce(int line) { return canProduceOnLine != null && canProduceOnLine[line]; }
boolean isProduced() { return !isPurchased; }
boolean isRoot() { return hasExternalDemand; }
int supplierCount() { return suppliers.size(); }
}
/**
* 供应商配置(用于采购品多供应商场景)
*/
static class Supplier {
String name; // 供应商名称
double purchaseCost; // 采购单价
int leadTime; // 采购提前期(天)
double maxSupplyPerDay; // 每日最大供应能力
double minPurchaseLot; // 最小采购批量
double lotMultiple; // 采购批量步长
boolean enableLotMultiple; // 是否启用批量步长
Supplier(String name) {
this.name = name;
}
}
// ========== 通用方法 ==========
/**
* 递归创建所有节点的变量
*/
static void createAllVariables(MPSolver solver, List<ProductNode> nodes, int numDays) {
for (ProductNode node : nodes) {
createNodeVariables(solver, node, numDays);
}
}
/**
* 创建单个节点的变量
*/
static void createNodeVariables(MPSolver solver, ProductNode node, int numDays) {
if (node.variablesCreated) return;
node.variablesCreated = true;
int numLines = node.lineCount();
// 所有节点都有库存变量
node.inventory = new MPVariable[1][numDays];
for (int t = 0; t < numDays; t++) {
node.inventory[0][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
"inv_" + node.name + "_d" + (t + 1));
}
if (node.isPurchased) {
// 采购品:多供应商采购变量 [supplier][0][day]
int numSuppliers = node.supplierCount();
if (numSuppliers > 0) {
node.purchase = new MPVariable[numSuppliers][1][numDays];
node.purchaseSwitch = new MPVariable[numSuppliers][numDays];
// 检查是否有供应商启用了批量步长
boolean hasLotMultipleSupplier = false;
for (Supplier s : node.suppliers) {
if (s.enableLotMultiple && s.lotMultiple > 0) {
hasLotMultipleSupplier = true;
break;
}
}
if (hasLotMultipleSupplier) {
node.supplierLotMultipleVar = new MPVariable[numSuppliers][1][numDays];
}
for (int s = 0; s < numSuppliers; s++) {
Supplier supplier = node.suppliers.get(s);
boolean needSwitch = supplier.minPurchaseLot > 0;
boolean needLotMult = supplier.enableLotMultiple && supplier.lotMultiple > 0;
for (int t = 0; t < numDays; t++) {
node.purchase[s][0][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
"pr_" + node.name + "_" + supplier.name + "_d" + (t + 1));
if (needSwitch) {
node.purchaseSwitch[s][t] = solver.makeBoolVar(
"prSw_" + node.name + "_" + supplier.name + "_d" + (t + 1));
}
if (needLotMult && node.supplierLotMultipleVar != null) {
node.supplierLotMultipleVar[s][0][t] = solver.makeIntVar(0,
Double.POSITIVE_INFINITY,
"suppLotK_" + node.name + "_" + supplier.name + "_d" + (t + 1));
}
}
}
} else {
// 无供应商配置,使用默认单一采购变量(向后兼容)
node.purchase = new MPVariable[1][1][numDays];
for (int t = 0; t < numDays; t++) {
node.purchase[0][0][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
"pr_" + node.name + "_d" + (t + 1));
}
}
} else {
// 生产品:产量 + 开关变量
if (numLines > 0 && node.canProduceOnLine != null) {
node.production = new MPVariable[numLines][numDays];
node.switchVar = new MPVariable[numLines][numDays];
// 如果启用了批量步长约束,创建整数变量
if (node.enableLotMultiple && node.lotMultiple > 0) {
node.lotMultipleVar = new MPVariable[numLines][1][numDays];
}
for (int l = 0; l < numLines; l++) {
if (node.canProduce(l)) {
for (int t = 0; t < numDays; t++) {
node.production[l][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
"xp_" + node.name + "_L" + (l + 1) + "_d" + (t + 1));
node.switchVar[l][t] = solver.makeBoolVar(
"sw_" + node.name + "_L" + (l + 1) + "_d" + (t + 1));
// 创建批量步长整数变量 k (>= 0)
if (node.enableLotMultiple && node.lotMultiple > 0
&& node.lotMultipleVar != null) {
node.lotMultipleVar[l][0][t] = solver.makeIntVar(0,
Double.POSITIVE_INFINITY,
"lotK_" + node.name + "_L" + (l + 1) + "_d" + (t + 1));
}
}
}
}
}
}
// 外部需求节点:shortfall 变量
if (node.hasExternalDemand && node.shortfallPenalty > 0) {
node.shortfall = new MPVariable[1][numDays];
for (int t = 0; t < numDays; t++) {
node.shortfall[0][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
"short_" + node.name + "_d" + (t + 1));
}
}
// 低于安全库存的惩罚变量(若设置了安全库存)
if (node.safetyStock > 0) {
node.underSafety = new MPVariable[1][numDays];
for (int t = 0; t < numDays; t++) {
node.underSafety[0][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
"underSafe_" + node.name + "_d" + (t + 1));
}
}
// 超过最大库存的惩罚变量(若设置了最大库存和惩罚)
if (node.maxStock > 0 && node.overstockPenalty > 0) {
node.overMaxStock = new MPVariable[1][numDays];
for (int t = 0; t < numDays; t++) {
node.overMaxStock[0][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
"overMax_" + node.name + "_d" + (t + 1));
}
}
}
/**
* 添加所有节点的目标函数系数
*/
static void addAllObjectiveTerms(MPObjective obj, List<ProductNode> nodes, int numDays) {
for (ProductNode node : nodes) {
addNodeObjective(obj, node, numDays);
}
}
/**
* 添加单个节点的目标函数系数
*/
static void addNodeObjective(MPObjective obj, ProductNode node, int numDays) {
int numLines = node.lineCount();
if (node.isPurchased && node.purchase != null) {
// 多供应商采购成本
int numSuppliers = node.supplierCount();
for (int s = 0; s < (numSuppliers > 0 ? numSuppliers : 1); s++) {
double cost = (numSuppliers > 0 && s < numSuppliers)
? node.suppliers.get(s).purchaseCost : node.prodCost;
for (int t = 0; t < numDays; t++) {
obj.setCoefficient(node.purchase[s][0][t], cost);
}
}
} else if (node.production != null) {
// 生产成本 + 换型成本
for (int l = 0; l < numLines; l++) {
if (node.canProduce(l)) {
for (int t = 0; t < numDays; t++) {
obj.setCoefficient(node.production[l][t], node.prodCost);
obj.setCoefficient(node.switchVar[l][t], node.setupCost);
}
}
}
}
// 库存持有成本
for (int t = 0; t < numDays; t++) {
obj.setCoefficient(node.inventory[0][t], node.holdCost);
}
// shortfall 惩罚成本
if (node.shortfall != null) {
for (int t = 0; t < numDays; t++) {
obj.setCoefficient(node.shortfall[0][t], node.shortfallPenalty);
}
}
// 低于安全库存惩罚(按 shortfallPenalty 的 50% 计算)
if (node.underSafety != null) {
double safetyPenalty = node.shortfallPenalty * 0.5;
for (int t = 0; t < numDays; t++) {
obj.setCoefficient(node.underSafety[0][t], safetyPenalty);
}
}
// 超过最大库存惩罚
if (node.overMaxStock != null) {
for (int t = 0; t < numDays; t++) {
obj.setCoefficient(node.overMaxStock[0][t], node.overstockPenalty);
}
}
}
/**
* 添加所有节点的库存平衡约束
*/
static void addAllInventoryBalance(MPSolver solver, List<ProductNode> nodes, int numDays) {
for (ProductNode node : nodes) {
addNodeInventoryBalance(solver, node, numDays);
}
}
/**
* 统一库存平衡约束
* 公式:上期库存 + 产量 + 采购(按提前期到货) + shortfall = 需求 + 消耗 + 期末库存
* 移项后:上期库存 + 产量 + 采购 + shortfall - 消耗 - 期末库存 = 需求
*/
static void addNodeInventoryBalance(MPSolver solver, ProductNode node, int numDays) {
for (int t = 0; t < numDays; t++) {
double rhs = 0;
// 外部需求作为 rhs
if (node.hasExternalDemand && node.demand != null) {
rhs = node.demand[t];
}
// 创建约束,右端为 rhs
MPConstraint c = solver.makeConstraint(rhs, rhs,
"inv_" + node.name + "_d" + (t + 1));
// 1. 上期库存(正)
if (t == 0) {
rhs -= node.initInv;
c.setBounds(rhs, rhs);
} else {
c.setCoefficient(node.inventory[0][t - 1], 1);
}
// 2. 本期产量(正)
if (node.isProduced() && node.production != null) {
for (int l = 0; l < node.lineCount(); l++) {
if (node.canProduce(l)) {
c.setCoefficient(node.production[l][t], 1);
}
}
}
// 3. 本期采购到货(正)- 支持多供应商不同提前期
if (node.isPurchased && node.purchase != null) {
int numSuppliers = node.supplierCount();
if (numSuppliers > 0) {
// 多供应商:按各自提前期到货
for (int s = 0; s < numSuppliers; s++) {
Supplier supplier = node.suppliers.get(s);
int leadTime = supplier.leadTime;
// 采购在 day t-leadTime 到货于 day t
int purchaseDay = t - leadTime;
if (purchaseDay >= 0 && purchaseDay < numDays) {
c.setCoefficient(node.purchase[s][0][purchaseDay], 1);
}
}
} else {
// 单采购变量(向后兼容,提前期为0)
c.setCoefficient(node.purchase[0][0][t], 1);
}
}
// 4. shortfall 缺口(正,外部需求节点)
if (node.shortfall != null) {
c.setCoefficient(node.shortfall[0][t], 1);
}
// 5. 本期消耗(负):Σ(父节点产量 × BOM系数)
for (BomParentRef ref : node.bomParents) {
ProductNode parent = ref.parent;
if (parent.production != null) {
for (int l = 0; l < parent.lineCount(); l++) {
if (parent.canProduce(l) && parent.production[l][t] != null) {
c.setCoefficient(parent.production[l][t], -ref.coefficient);
}
}
}
}
// 6. 期末库存(负)
c.setCoefficient(node.inventory[0][t], -1);
}
}
/**
* 添加所有节点的产能约束
*/
static void addAllCapacityConstraints(MPSolver solver, List<ProductNode> nodes, int numDays) {
// 按产线分组,每条产线每天所有可生产产品的耗时之和 ≤ 产能
// 使用 Map<产线索引, List<节点>> 聚合
Map<Integer, List<ProductNode>> lineNodesMap = new HashMap<>();
for (ProductNode node : nodes) {
if (!node.isProduced() || node.production == null) continue;
for (int l = 0; l < node.lineCount(); l++) {
if (node.canProduce(l)) {
lineNodesMap.computeIfAbsent(l, k -> new ArrayList<>()).add(node);
}
}
}
for (Map.Entry<Integer, List<ProductNode>> entry : lineNodesMap.entrySet()) {
int lineIdx = entry.getKey();
List<ProductNode> lineNodes = entry.getValue();
double capacity = lineNodes.get(0).lineCapacity[lineIdx];
String lineName = lineNodes.get(0).lineNames[lineIdx];
for (int t = 0; t < numDays; t++) {
MPConstraint cap = solver.makeConstraint(0, capacity,
"cap_" + lineName + "_d" + (t + 1));
for (ProductNode node : lineNodes) {
cap.setCoefficient(node.production[lineIdx][t],
1.0 / node.prodRateByLine[lineIdx]);
}
}
}
}
/**
* 添加所有节点的生产开关约束
*/
static void addAllSwitchConstraints(MPSolver solver, List<ProductNode> nodes,
int numDays, double bigM) {
for (ProductNode node : nodes) {
if (node.isProduced() && node.production != null && node.switchVar != null) {
for (int l = 0; l < node.lineCount(); l++) {
if (node.canProduce(l)) {
for (int t = 0; t < numDays; t++) {
MPConstraint c = solver.makeConstraint(-Double.POSITIVE_INFINITY, 0,
"sw_" + node.name + "_L" + (l + 1) + "_d" + (t + 1));
c.setCoefficient(node.production[l][t], 1);
c.setCoefficient(node.switchVar[l][t], -bigM);
}
}
}
}
}
}
/**
* 添加所有节点的库存边界约束
* 包括:最小库存(硬约束)、最大库存(硬约束+惩罚)、安全库存(软约束+惩罚)
*/
static void addAllInventoryBoundsConstraints(MPSolver solver, List<ProductNode> nodes, int numDays) {
for (ProductNode node : nodes) {
addNodeInventoryBounds(solver, node, numDays);
}
}
/**
* 添加单个节点的库存边界约束
*/
static void addNodeInventoryBounds(MPSolver solver, ProductNode node, int numDays) {
for (int t = 0; t < numDays; t++) {
MPVariable inv = node.inventory[0][t];
// 1. 最小库存约束(硬约束)
if (node.minStock > 0) {
// inventory[t] >= minStock
MPConstraint minC = solver.makeConstraint(node.minStock, Double.POSITIVE_INFINITY,
"minInv_" + node.name + "_d" + (t + 1));
minC.setCoefficient(inv, 1);
}
// 2. 最大库存约束(硬约束 + 超库存惩罚变量)
if (node.maxStock > 0) {
if (node.overMaxStock != null) {
// inventory[t] - overMaxStock[t] <= maxStock
MPConstraint maxC = solver.makeConstraint(-Double.POSITIVE_INFINITY, node.maxStock,
"maxInv_" + node.name + "_d" + (t + 1));
maxC.setCoefficient(inv, 1);
maxC.setCoefficient(node.overMaxStock[0][t], -1);
} else {
// 硬约束:inventory[t] <= maxStock
MPConstraint maxC = solver.makeConstraint(-Double.POSITIVE_INFINITY, node.maxStock,
"maxInv_" + node.name + "_d" + (t + 1));
maxC.setCoefficient(inv, 1);
}
}
// 3. 安全库存约束(软约束 + 低于安全库存惩罚变量)
if (node.safetyStock > 0 && node.underSafety != null) {
// inventory[t] + underSafety[t] >= safetyStock
MPConstraint safeC = solver.makeConstraint(node.safetyStock, Double.POSITIVE_INFINITY,
"safeInv_" + node.name + "_d" + (t + 1));
safeC.setCoefficient(inv, 1);
safeC.setCoefficient(node.underSafety[0][t], 1);
}
}
}
/**
* 添加所有节点的生产批量约束(最小生产批量 + 批量步长)
*/
static void addAllLotSizeConstraints(MPSolver solver, List<ProductNode> nodes, int numDays) {
for (ProductNode node : nodes) {
addNodeLotSizeConstraints(solver, node, numDays);
}
}
/**
* 添加单个节点的生产批量约束
* 1. 最小生产批量:production >= switchVar * minLotSize
* 2. 批量步长:production = lotMultiple * kVar (启用时)
* kVar 为整数变量,表示批量倍数
*/
static void addNodeLotSizeConstraints(MPSolver solver, ProductNode node, int numDays) {
if (node.isPurchased || node.production == null) return;
if (node.minLotSize <= 0 && node.lotMultiple <= 0) return;
boolean hasMinLot = node.minLotSize > 0;
boolean hasLotMultiple = node.enableLotMultiple && node.lotMultiple > 0 && node.lotMultipleVar != null;
for (int l = 0; l < node.lineCount(); l++) {
if (!node.canProduce(l)) continue;
for (int t = 0; t < numDays; t++) {
// 1. 最小生产批量约束
if (hasMinLot) {
MPConstraint minLotC = solver.makeConstraint(-Double.POSITIVE_INFINITY, 0,
"minLot_" + node.name + "_L" + (l + 1) + "_d" + (t + 1));
minLotC.setCoefficient(node.production[l][t], 1);
minLotC.setCoefficient(node.switchVar[l][t], -node.minLotSize);
}
// 2. 批量步长约束:production = lotMultiple * kVar
// 即:production - lotMultiple * kVar = 0
if (hasLotMultiple && node.lotMultipleVar[l][0][t] != null) {
MPConstraint lotMultC = solver.makeConstraint(0, 0,
"lotMult_" + node.name + "_L" + (l + 1) + "_d" + (t + 1));
lotMultC.setCoefficient(node.production[l][t], 1);
lotMultC.setCoefficient(node.lotMultipleVar[l][0][t], -node.lotMultiple);
}
}
}
}
/**
* 添加供应商约束(最大供应能力 + 最小采购批量)
*/
static void addAllSupplierConstraints(MPSolver solver, List<ProductNode> nodes, int numDays) {
for (ProductNode node : nodes) {
if (!node.isPurchased || node.supplierCount() == 0) continue;
addSupplierCapacityConstraints(solver, node, numDays);
}
}
/**
* 添加单个节点的供应商约束
* 1. 供应商最大供应能力约束
* 2. 供应商最小采购批量约束
* 3. 供应商批量步长约束
*/
static void addSupplierCapacityConstraints(MPSolver solver, ProductNode node, int numDays) {
int numSuppliers = node.supplierCount();
if (node.purchase == null) return;
for (int s = 0; s < numSuppliers; s++) {
Supplier supplier = node.suppliers.get(s);
boolean hasMinLot = supplier.minPurchaseLot > 0
&& node.purchaseSwitch != null && node.purchaseSwitch[s][0] != null;
for (int t = 0; t < numDays; t++) {
if (hasMinLot) {
// 1. 最小采购批量约束:purchase >= purchaseSwitch * minPurchaseLot
MPConstraint minLotC = solver.makeConstraint(-Double.POSITIVE_INFINITY, 0,
"suppMinLot_" + node.name + "_" + supplier.name + "_d" + (t + 1));
minLotC.setCoefficient(node.purchase[s][0][t], 1);
minLotC.setCoefficient(node.purchaseSwitch[s][t], -supplier.minPurchaseLot);
// 2. 与开关联动的上限约束:purchase <= purchaseSwitch * maxSupplyPerDay
// 此约束同时保证:switch=0时purchase=0,switch=1时purchase<=max
if (supplier.maxSupplyPerDay > 0) {
MPConstraint maxLotC = solver.makeConstraint(-Double.POSITIVE_INFINITY, 0,
"suppMaxLot_" + node.name + "_" + supplier.name + "_d" + (t + 1));
maxLotC.setCoefficient(node.purchase[s][0][t], 1);
maxLotC.setCoefficient(node.purchaseSwitch[s][t], -supplier.maxSupplyPerDay);
}
} else {
// 无最小批量约束,使用简单上限约束
if (supplier.maxSupplyPerDay > 0) {
MPConstraint capC = solver.makeConstraint(0, supplier.maxSupplyPerDay,
"suppCap_" + node.name + "_" + supplier.name + "_d" + (t + 1));
capC.setCoefficient(node.purchase[s][0][t], 1);
}
}
// 3. 供应商批量步长约束:purchase = lotMultiple * kVar
if (supplier.enableLotMultiple && supplier.lotMultiple > 0
&& node.supplierLotMultipleVar != null
&& node.supplierLotMultipleVar[s][0][t] != null) {
MPConstraint lotMultC = solver.makeConstraint(0, 0,
"suppLotMult_" + node.name + "_" + supplier.name + "_d" + (t + 1));
lotMultC.setCoefficient(node.purchase[s][0][t], 1);
lotMultC.setCoefficient(node.supplierLotMultipleVar[s][0][t], -supplier.lotMultiple);
}
}
}
}
/**
* 构建BOM父子引用关系
*/
static void buildParentReferences(List<ProductNode> nodes) {
for (ProductNode node : nodes) {
for (BomChild child : node.bomChildren) {
BomParentRef ref = new BomParentRef(node, child.coefficient);
child.child.bomParents.add(ref);
}
}
}
/**
* 按产线打印生产计划(产线视图)
*/
static void printByLineView(List<ProductNode> nodes, int day) {
// 按产线分组聚合
Map<Integer, List<ProductNode>> lineNodesMap = new HashMap<>();
for (ProductNode node : nodes) {
if (node.isPurchased || node.production == null) continue;
for (int l = 0; l < node.lineCount(); l++) {
if (node.canProduce(l)) {
lineNodesMap.computeIfAbsent(l, k -> new ArrayList<>()).add(node);
}
}
}
for (Map.Entry<Integer, List<ProductNode>> entry : lineNodesMap.entrySet()) {
int lineIdx = entry.getKey();
List<ProductNode> lineNodes = entry.getValue();
String lineName = lineNodes.get(0).lineNames[lineIdx];
double capacity = lineNodes.get(0).lineCapacity[lineIdx];
System.out.printf(" 【%s】产能%.0fh %n", lineName, capacity);
double totalHours = 0;
boolean hasProduction = false;
for (ProductNode node : lineNodes) {
double qty = node.production[lineIdx][day].solutionValue();
if (qty > 0.001) {
hasProduction = true;
double hours = qty / node.prodRateByLine[lineIdx];
double setupCost = node.switchVar[lineIdx][day].solutionValue() * node.setupCost;
totalHours += hours;
System.out.printf(" %s: %.0f件 (耗时%.1fh, 换型%.0f元)%n",
node.name, qty, hours, setupCost);
}
}
if (!hasProduction) {
System.out.println(" (休息)");
} else {
System.out.printf(" 利用率: %.0f/%.0f h (%.0f%%)%n",
totalHours, capacity, (totalHours / capacity) * 100);
}
}
}
/**
* 按产品打印生产计划(产品视图)
*/
static void printByProductView(List<ProductNode> nodes, int day) {
for (ProductNode node : nodes) {
// 库存信息
double inv = node.inventory[0][day].solutionValue();
List<String> constraints = new ArrayList<>();
if (node.safetyStock > 0) constraints.add(String.format("安全%.0f", node.safetyStock));
if (node.minStock > 0) constraints.add(String.format("最小%.0f", node.minStock));
if (node.maxStock > 0) constraints.add(String.format("最大%.0f", node.maxStock));
if (node.minLotSize > 0) constraints.add(String.format("最小批量%.0f", node.minLotSize));
String constraintStr = constraints.isEmpty() ? "" : " [" + String.join("/", constraints) + "]";
// 库存状态标识
String status = "";
if (node.minStock > 0 && inv < node.minStock) {
status = " ⚠低于最小库存";
} else if (node.safetyStock > 0 && inv < node.safetyStock) {
status = " ⚠低于安全库存";
} else if (node.maxStock > 0 && inv > node.maxStock) {
status = " ⚠超过最大库存";
}
if (node.isPurchased && node.purchase != null) {
// 多供应商采购
int numSuppliers = node.supplierCount();
if (numSuppliers > 0) {
List<String> supplierDetails = new ArrayList<>();
double totalQty = 0;
for (int s = 0; s < numSuppliers; s++) {
Supplier supplier = node.suppliers.get(s);
double qty = node.purchase[s][0][day].solutionValue();
if (qty > 0.001) {
totalQty += qty;
double cost = qty * supplier.purchaseCost;
supplierDetails.add(String.format("%s(%.0f件,%.1f元)",
supplier.name, qty, cost));
}
}
if (totalQty > 0.001) {
System.out.printf(" %s: 库存%.0f件%s%s | 采购共%.0f件 %s%n",
node.name, inv, constraintStr, status, totalQty,
"[" + String.join(", ", supplierDetails) + "]");
} else {
System.out.printf(" %s: 库存%.0f件%s%s | 无采购%n",
node.name, inv, constraintStr, status);
}
} else {
// 向后兼容:单一采购变量
double qty = node.purchase[0][0][day].solutionValue();
if (qty > 0.001) {
System.out.printf(" %s: 库存%.0f件%s%s | 采购到货 %.0f 件(成本 %.0f元)%n",
node.name, inv, constraintStr, status, qty, qty * node.prodCost);
} else {
System.out.printf(" %s: 库存%.0f件%s%s | 无采购%n",
node.name, inv, constraintStr, status);
}
}
} else if (node.production != null) {
double totalQty = 0;
List<String> lineDetails = new ArrayList<>();
for (int l = 0; l < node.lineCount(); l++) {
if (node.canProduce(l)) {
double qty = node.production[l][day].solutionValue();
if (qty > 0.001) {
totalQty += qty;
double hours = qty / node.prodRateByLine[l];
lineDetails.add(String.format("L%d(%.0f件,%.1fh)", l + 1, qty, hours));
}
}
}
if (totalQty > 0.001) {
System.out.printf(" %s: 库存%.0f件%s%s | 生产共%.0f件 [",
node.name, inv, constraintStr, status, totalQty);
System.out.println(String.join(", ", lineDetails) + "]");
} else {
System.out.printf(" %s: 库存%.0f件%s%s | 无生产%n",
node.name, inv, constraintStr, status);
}
}
}
}
/**
* 打印节点的shortfall信息(含原因分析)
*/
static void printNodeShortfall(ProductNode node, int day, List<ProductNode> allNodes) {
if (node.shortfall != null) {
double sht = node.shortfall[0][day].solutionValue();
if (sht > 0.001) {
System.out.printf(" ⚠ %s 未满足需求:%.0f 件%n", node.name, sht);
List<String> reasons = analyzeShortfallReasons(node, day, allNodes, sht);
for (String reason : reasons) {
System.out.printf(" └ %s%n", reason);
}
}
}
}
/**
* 分析需求未满足的原因
* @param node 产品节点
* @param day 天数
* @param allNodes 所有节点
* @param shortfall 缺口数量
* @return 原因列表
*/
static List<String> analyzeShortfallReasons(ProductNode node, int day,
List<ProductNode> allNodes, double shortfall) {
List<String> reasons = new ArrayList<>();
// 计算当前产量和可达到的最大产量
double totalProduction = 0;
double maxPossibleProduction = 0;
StringBuilder lineAnalysis = new StringBuilder();
if (!node.isPurchased && node.production != null) {
for (int l = 0; l < node.lineCount(); l++) {
if (!node.canProduce(l)) continue;
double prod = node.production[l][day].solutionValue();
totalProduction += prod;
// 计算该产线的剩余产能
double capacity = node.lineCapacity[l];
double usedHours = prod > 0.001 ? prod / node.prodRateByLine[l] : 0;
double remainingHours = capacity - usedHours;
double maxAdditional = remainingHours * node.prodRateByLine[l];
maxPossibleProduction += prod + maxAdditional;
// 检查产线利用率
double utilization = (usedHours / capacity) * 100;
if (prod > 0.001 && utilization >= 95) {
lineAnalysis.append(String.format("L%d(%.0f%%利用) ", l + 1));
}
}
if (lineAnalysis.length() > 0) {
reasons.add("产线产能已满: " + lineAnalysis.toString().trim()
+ String.format(",仅生产%.0f件", totalProduction));
}
// 原因2: 最小生产批量约束导致无法生产
if (totalProduction < 0.001 && node.minLotSize > 0) {
List<String> blockedLines = new ArrayList<>();
for (int l = 0; l < node.lineCount(); l++) {
if (!node.canProduce(l)) continue;
double remainingCap = node.lineCapacity[l];
double canProduce = remainingCap * node.prodRateByLine[l];
if (canProduce < node.minLotSize) {
blockedLines.add(String.format("L%d(产能仅能生产%.0f件<批量%.0f件)",
l + 1, canProduce, node.minLotSize));
}
}
if (!blockedLines.isEmpty()) {
reasons.add("最小生产批量约束: " + String.join(", ", blockedLines));
} else {
reasons.add(String.format("最小生产批量约束: 需求%.0f件<批量%.0f件,模型选择不生产",
shortfall + totalProduction, node.minLotSize));
}
}
// 原因3: 原材料/半成品瓶颈分析
if (!node.bomChildren.isEmpty()) {
List<String> materialBottlenecks = analyzeMaterialBottlenecks(node, day, shortfall, totalProduction);
reasons.addAll(materialBottlenecks);
}
}
// 原因4: 供应商供应能力限制(采购品)
if (node.isPurchased && node.purchase != null && node.supplierCount() > 0) {
List<String> supplierLimits = analyzeSupplierLimits(node, day);
reasons.addAll(supplierLimits);
}
// 如果没有具体原因,给出总体判断
if (reasons.isEmpty()) {
if (maxPossibleProduction < shortfall + totalProduction) {
reasons.add(String.format("综合产能不足: 最大可生产%.0f件,需求%.0f件",
maxPossibleProduction, shortfall + totalProduction));
} else {
reasons.add("多因素综合影响(产能/物料/成本优化)");
}
}
return reasons;
}
/**
* 分析原材料/半成品瓶颈
*/
static List<String> analyzeMaterialBottlenecks(ProductNode node, int day,
double shortfall, double currentProduction) {
List<String> reasons = new ArrayList<>();
for (BomChild bomChild : node.bomChildren) {
ProductNode child = bomChild.child;
double needPerUnit = bomChild.coefficient;
// 计算当前产量对该物料的消耗
double currentNeed = currentProduction * needPerUnit;
// 满足需求的总消耗
double totalNeed = (shortfall + currentProduction) * needPerUnit;
if (child.inventory == null) continue;
double childInv = child.inventory[0][day].solutionValue();
// 计算该物料的当前可用量(库存+当日产量/采购)
double childAvailable = childInv;
double childProduced = 0;
if (!child.isPurchased && child.production != null) {
for (int l = 0; l < child.lineCount(); l++) {
if (child.canProduce(l)) {
childProduced += child.production[l][day].solutionValue();
}
}
childAvailable += childProduced;
} else if (child.isPurchased && child.purchase != null) {
for (int s = 0; s < child.supplierCount(); s++) {
childAvailable += child.purchase[s][0][day].solutionValue();
}
if (child.supplierCount() == 0) {
childAvailable += child.purchase[0][0][day].solutionValue();
}
}
// 检查物料是否成为瓶颈
double gap = totalNeed - childAvailable;
if (gap > 0.001 && childInv < currentNeed) {
// 库存不足以覆盖当前消耗
reasons.add(String.format("物料瓶颈: %s(库存%.0f件,消耗%.0f件,需求%.0f件,缺%.0f件)",
child.name, childInv, currentNeed, totalNeed, gap));
} else if (gap > 0.001 && child.isPurchased && child.supplierCount() > 0) {
// 采购品供应不足
double totalPurchased = 0;
for (int s = 0; s < child.supplierCount(); s++) {
totalPurchased += child.purchase[s][0][day].solutionValue();
}
reasons.add(String.format("采购品供应不足: %s(库存%.0f件,采购%.0f件,总供%.0f件,缺%.0f件)",
child.name, childInv, totalPurchased, childAvailable, gap));
}
}
return reasons;
}
/**
* 分析供应商供应能力限制
*/
static List<String> analyzeSupplierLimits(ProductNode node, int day) {
List<String> reasons = new ArrayList<>();
if (node.suppliers.isEmpty()) return reasons;
for (int s = 0; s < node.supplierCount(); s++) {
Supplier supplier = node.suppliers.get(s);
double purchased = node.purchase[s][0][day].solutionValue();
// 检查是否达到供应上限
if (supplier.maxSupplyPerDay > 0 && purchased >= supplier.maxSupplyPerDay * 0.98) {
reasons.add(String.format("%s供应已满: %s(供%.0f件/日,已购%.0f件)",
node.name, supplier.name, supplier.maxSupplyPerDay, purchased));
}
// 检查是否因最小批量未采购
if (purchased < 0.001 && supplier.minPurchaseLot > 0) {
// 需求不足以达到最小批量
double needed = 0;
for (BomParentRef parent : node.bomParents) {
if (parent.parent.production != null) {
needed += parent.parent.production[0][day].solutionValue() * parent.coefficient;
}
}
if (needed > 0 && needed < supplier.minPurchaseLot) {
reasons.add(String.format("采购批量约束: %s需求%.0f件<%s最小批量%.0f件",
node.name, needed, supplier.name, supplier.minPurchaseLot));
}
}
}
return reasons;
}
/**
* 打印节点库存
*/
static void printNodeInventory(ProductNode node, int day) {
double inv = node.inventory[0][day].solutionValue();
System.out.printf("%s=%.0f ", node.name, inv);
}
/**
* 计算节点成本
*/
static double[] calcNodeCost(ProductNode node, int numDays) {
double prodCost = 0, holdCost = 0, setupCost = 0, shortfallCost = 0, purchaseCost = 0;
for (int t = 0; t < numDays; t++) {
if (node.isPurchased && node.purchase != null) {
// 多供应商采购成本
int numSuppliers = node.supplierCount();
if (numSuppliers > 0) {
for (int s = 0; s < numSuppliers; s++) {
Supplier supplier = node.suppliers.get(s);
purchaseCost += node.purchase[s][0][t].solutionValue() * supplier.purchaseCost;
}
} else {
purchaseCost += node.purchase[0][0][t].solutionValue() * node.prodCost;
}
} else if (node.production != null) {
for (int l = 0; l < node.lineCount(); l++) {
if (node.canProduce(l)) {
prodCost += node.production[l][t].solutionValue() * node.prodCost;
setupCost += node.switchVar[l][t].solutionValue() * node.setupCost;
}
}
}
holdCost += node.inventory[0][t].solutionValue() * node.holdCost;
if (node.shortfall != null) {
shortfallCost += node.shortfall[0][t].solutionValue() * node.shortfallPenalty;
}
}
return new double[]{prodCost, holdCost, setupCost, purchaseCost, shortfallCost};
}
// ========== 构建BOM树的辅助方法 ==========
/**
* 构建一个简单的3层BOM示例
* P1 → S1,R1 → R2,R3
*/
static List<ProductNode> buildExampleBom(int numDays, int purchaseLeadTime) {
List<ProductNode> nodes = new ArrayList<>();
// 公共产线配置(3条产线)
String[] lineNames = {"L1", "L2", "L3"};
double[] lineCapacity = {10, 10, 10};
// ========== 成品 P1(外部需求,可在L1或L3生产) ==========
ProductNode p1 = new ProductNode("P1");
p1.prodCost = 10;
p1.setupCost = 200;
p1.holdCost = 1.0;
p1.initInv = 20;
p1.shortfallPenalty = 1000;
p1.safetyStock = 20; // 安全库存:20件
p1.minStock = 5; // 最小库存:5件
p1.maxStock = 150; // 最大库存:150件
p1.overstockPenalty = 50; // 超库存惩罚:50元/件
p1.isPurchased = false;
p1.hasExternalDemand = true;
p1.lineNames = lineNames;
p1.lineCapacity = lineCapacity;
p1.canProduceOnLine = new boolean[]{true, false, true};
p1.prodRateByLine = new double[]{10, 0, 8};
p1.demand = new double[]{50, 60, 40};
p1.minLotSize = 20; // 最小生产批量:20件
p1.lotMultiple = 10; // 批量步长:10件(需启用)
p1.enableLotMultiple = false; // 是否启用批量步长约束
// ========== 成品 P2(外部需求,可在L1或L3生产) ==========
ProductNode p2 = new ProductNode("P2");
p2.prodCost = 15;
p2.setupCost = 300;
p2.holdCost = 1.5;
p2.initInv = 10;
p2.shortfallPenalty = 1000;
p2.safetyStock = 15; // 安全库存:15件
p2.minStock = 3; // 最小库存:3件
p2.maxStock = 120; // 最大库存:120件
p2.overstockPenalty = 60; // 超库存惩罚:60元/件
p2.isPurchased = false;
p2.hasExternalDemand = true;
p2.lineNames = lineNames;
p2.lineCapacity = lineCapacity;
p2.canProduceOnLine = new boolean[]{true, false, true};
p2.prodRateByLine = new double[]{8, 0, 6};
p2.demand = new double[]{30, 40, 50};
p2.minLotSize = 15; // 最小生产批量:15件
p2.lotMultiple = 5; // 批量步长:5件(需启用)
p2.enableLotMultiple = false; // 是否启用批量步长约束
// ========== 半成品 S1(生产品,在L2生产) ==========
ProductNode s1 = new ProductNode("S1");
s1.prodCost = 3;
s1.setupCost = 80;
s1.holdCost = 0.3;
s1.initInv = 50;
s1.safetyStock = 30; // 安全库存:30件
s1.minStock = 10; // 最小库存:10件
s1.maxStock = 200; // 最大库存:200件
s1.overstockPenalty = 20; // 超库存惩罚:20元/件
s1.isPurchased = false;
s1.hasExternalDemand = false;
s1.lineNames = lineNames;
s1.lineCapacity = lineCapacity;
s1.canProduceOnLine = new boolean[]{false, true, false};
s1.prodRateByLine = new double[]{0, 30, 0};
s1.minLotSize = 30; // 最小生产批量:30件
s1.lotMultiple = 0; // 不启用批量步长
// ========== 原材料 R1(采购品,多供应商) ==========
ProductNode r1 = new ProductNode("R1");
r1.prodCost = 1;
r1.holdCost = 0.1;
r1.initInv = 200;
r1.safetyStock = 100; // 安全库存:100件
r1.minStock = 50; // 最小库存:50件
r1.maxStock = 500; // 最大库存:500件
r1.overstockPenalty = 10; // 超库存惩罚:10元/件
r1.isPurchased = true;
r1.hasExternalDemand = false;
// 多供应商配置
Supplier r1Sup1 = new Supplier("供应商A");
r1Sup1.purchaseCost = 1.0; // 单价1元/件
r1Sup1.leadTime = 1; // 提前期1天
r1Sup1.maxSupplyPerDay = 200; // 日最大供应200件
r1Sup1.minPurchaseLot = 50; // 最小采购批量50件
r1Sup1.lotMultiple = 0; // 不启用批量步长
Supplier r1Sup2 = new Supplier("供应商B");
r1Sup2.purchaseCost = 0.9; // 单价0.9元/件(更便宜)
r1Sup2.leadTime = 2; // 提前期2天(更长)
r1Sup2.maxSupplyPerDay = 100; // 日最大供应100件
r1Sup2.minPurchaseLot = 30; // 最小采购批量30件
r1Sup2.lotMultiple = 10; // 批量步长10件
r1Sup2.enableLotMultiple = false;
r1.suppliers.add(r1Sup1);
r1.suppliers.add(r1Sup2);
// ========== 原材料 R2(采购品,多供应商) ==========
ProductNode r2 = new ProductNode("R2");
r2.prodCost = 1.5;
r2.holdCost = 0.15;
r2.initInv = 100;
r2.safetyStock = 50; // 安全库存:50件
r2.minStock = 20; // 最小库存:20件
r2.maxStock = 400; // 最大库存:400件
r2.overstockPenalty = 12; // 超库存惩罚:12元/件
r2.isPurchased = true;
r2.hasExternalDemand = false;
// 多供应商配置
Supplier r2Sup1 = new Supplier("供应商甲");
r2Sup1.purchaseCost = 1.5; // 单价1.5元/件
r2Sup1.leadTime = 1; // 提前期1天
r2Sup1.maxSupplyPerDay = 150; // 日最大供应150件
r2Sup1.minPurchaseLot = 40; // 最小采购批量40件
Supplier r2Sup2 = new Supplier("供应商乙");
r2Sup2.purchaseCost = 1.3; // 单价1.3元/件(更便宜)
r2Sup2.leadTime = 3; // 提前期3天(更长)
r2Sup2.maxSupplyPerDay = 80; // 日最大供应80件
r2Sup2.minPurchaseLot = 20; // 最小采购批量20件
r2.suppliers.add(r2Sup1);
r2.suppliers.add(r2Sup2);
// ========== 构建BOM关系 ==========
// P1 → S1(2件), R1(3件) — 成品同时消耗半成品和原材料
p1.bomChildren.add(new BomChild(s1, 2));
p1.bomChildren.add(new BomChild(r1, 3));
// P2 → S1(1件), S2(1件)
p2.bomChildren.add(new BomChild(s1, 1));
// S1 → R2(2件), R3(1件)
s1.bomChildren.add(new BomChild(r2, 2));
// ========== 添加所有节点 ==========
nodes.add(p1);
nodes.add(p2);
nodes.add(s1);
nodes.add(r1);
nodes.add(r2);
// 构建父子引用
buildParentReferences(nodes);
return nodes;
}
// ========== 主方法 ==========
public static void main(String[] args) {
Loader.loadNativeLibraries();
// ========== 1. 维度定义 ==========
int numDays = 3;
double bigM = 10000;
// ========== 2. 构建BOM树 ==========
List<ProductNode> nodes = buildExampleBom(numDays, 1);
// 统计信息
List<ProductNode> rootNodes = new ArrayList<>();
List<ProductNode> producedNodes = new ArrayList<>();
List<ProductNode> purchasedNodes = new ArrayList<>();
for (ProductNode node : nodes) {
if (node.isRoot()) rootNodes.add(node);
if (node.isPurchased) purchasedNodes.add(node);
else producedNodes.add(node);
}
// ========== 3. 创建求解器 ==========
MPSolver solver = MPSolver.createSolver("CBC");
// ========== 4. 创建变量 ==========
createAllVariables(solver, nodes, numDays);
// ========== 5. 目标函数 ==========
MPObjective obj = solver.objective();
addAllObjectiveTerms(obj, nodes, numDays);
obj.setMinimization();
// ========== 6. 约束条件 ==========
addAllInventoryBalance(solver, nodes, numDays);
addAllCapacityConstraints(solver, nodes, numDays);
addAllSwitchConstraints(solver, nodes, numDays, bigM);
addAllInventoryBoundsConstraints(solver, nodes, numDays);
addAllLotSizeConstraints(solver, nodes, numDays);
addAllSupplierConstraints(solver, nodes, numDays);
// ========== 7. 求解 ==========
System.out.println("========== 网状BOM + MRP 排产求解 ==========");
System.out.printf("成品(根节点) %d 种:", rootNodes.size());
for (ProductNode n : rootNodes) System.out.print(n.name + " ");
System.out.printf("%n生产品 %d 种:", producedNodes.size());
for (ProductNode n : producedNodes) System.out.print(n.name + " ");
System.out.printf("%n采购品 %d 种:", purchasedNodes.size());
for (ProductNode n : purchasedNodes) System.out.print(n.name + " ");
System.out.printf("%n周期 %d 天%n%n", numDays);
MPSolver.ResultStatus status = solver.solve();
// ========== 8. 结果输出 ==========
if (status == MPSolver.ResultStatus.OPTIMAL) {
System.out.println("✅ 求解成功!全局最优解");
System.out.printf("最小总成本:%.2f 元%n%n", obj.value());
boolean hasShortfall = false;
for (int t = 0; t < numDays; t++) {
System.out.println("═══════════════════ 第 " + (t + 1) + " 天 ═══════════════════");
// 产线视图
System.out.println(" ▶ 产线视图(按产线分组)");
printByLineView(nodes, t);
// 产品视图
System.out.println(" ▶ 产品视图(汇总各产线产量)");
printByProductView(nodes, t);
// 输出shortfall(含原因分析)
for (ProductNode node : rootNodes) {
if (node.shortfall != null && node.shortfall[0][t].solutionValue() > 0.001) {
printNodeShortfall(node, t, nodes);
hasShortfall = true;
}
}
// 库存
System.out.println(" ▶ 期末库存");
System.out.print(" ");
for (ProductNode node : nodes) {
printNodeInventory(node, t);
}
System.out.println();
System.out.println();
}
// ========== 9. 成本明细 ==========
double totalProdCost = 0, totalHoldCost = 0, totalSetupCost = 0;
double totalPurchaseCost = 0, totalShortfallCost = 0;
for (ProductNode node : nodes) {
double[] cost = calcNodeCost(node, numDays);
totalProdCost += cost[0];
totalHoldCost += cost[1];
totalSetupCost += cost[2];
totalPurchaseCost += cost[3];
totalShortfallCost += cost[4];
}
System.out.println("═══════════════════ 成本明细 ═══════════════════");
System.out.printf("生产成本:%10.2f%n", totalProdCost);
System.out.printf("换型成本:%10.2f%n", totalSetupCost);
System.out.printf("采购成本:%10.2f%n", totalPurchaseCost);
System.out.printf("库存成本:%10.2f%n", totalHoldCost);
if (totalShortfallCost > 0.001) {
System.out.printf("⚠ 缺口惩罚:%10.2f%n", totalShortfallCost);
hasShortfall = true;
}
System.out.printf("──────────────────────────────%n");
System.out.printf("总 成 本:%10.2f 元%n", obj.value());
if (hasShortfall) {
System.out.println();
System.out.println("⚠ 注意:存在需求缺口,部分订单未满足。");
}
System.out.println();
System.out.println("═══════════════════ 求解统计 ═══════════════════");
System.out.println("变量数:" + solver.numVariables());
System.out.println("约束数:" + solver.numConstraints());
System.out.printf("耗时:%.3f 秒%n", solver.wallTime() / 1000.0);
} else if (status == MPSolver.ResultStatus.INFEASIBLE) {
System.out.println("❌ 无解");
} else {
System.out.println("求解状态:" + status);
}
}
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
package com.aps.service.mp;
import com.google.ortools.linearsolver.MPSolver;
import com.google.ortools.linearsolver.MPVariable;
/**
* 作者:佟礼
* 时间:2026-07-29
*/
public class InitVariablesProduct {
/**
* 创建单层产品的决策变量
* @param solver 求解器
* @param config 层配置
* @param numDays 天数
* @param isPurchaseLayer 是否为采购层(原材料层,无生产变量和开关变量)
* @return 层变量
*/
static ProductLayerVariables createVariables(MPSolver solver, ProductLayerConfig config,
int numDays, boolean isPurchaseLayer) {
ProductLayerVariables vars = new ProductLayerVariables();
int n = config.size();
if (isPurchaseLayer) {
// 原材料层:只有采购量和库存变量
vars.purchase = new MPVariable[n][numDays];
vars.inventory = new MPVariable[n][numDays];
for (int i = 0; i < n; i++) {
for (int t = 0; t < numDays; t++) {
vars.purchase[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
"pr_" + config.names[i] + "_d" + (t + 1));
vars.inventory[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
"ir_" + config.names[i] + "_d" + (t + 1));
}
}
} else {
// 生产层:产量、库存、开关变量
vars.production = new MPVariable[n][numDays];
vars.inventory = new MPVariable[n][numDays];
vars.switchVar = new MPVariable[n][numDays];
String prefix = config.names[0].startsWith("P") ? "xp" : "xs";
String invPrefix = config.names[0].startsWith("P") ? "ip" : "is";
String swPrefix = config.names[0].startsWith("P") ? "yp" : "ys";
for (int i = 0; i < n; i++) {
for (int t = 0; t < numDays; t++) {
vars.production[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
prefix + "_" + config.names[i] + "_d" + (t + 1));
vars.inventory[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
invPrefix + "_" + config.names[i] + "_d" + (t + 1));
vars.switchVar[i][t] = solver.makeBoolVar(
swPrefix + "_" + config.names[i] + "_d" + (t + 1));
}
}
}
return vars;
}
}
package com.aps.service.mp;
import com.aps.common.util.FileHelper;
import com.google.ortools.Loader;
import com.google.ortools.linearsolver.*;
/**
* 作者:佟礼
* 时间:2026-07-23
* MIP 主生产排程 Demo
* 场景:1条产线,2种产品,2天排产
* 目标:最小化 生产成本 + 库存持有成本 + 换型成本
*/
public class MpsSchedulingDemo {
public static void main(String[] args) {
// 1. 加载 OR-Tools 本地库(必须)
Loader.loadNativeLibraries();
// ========== 2. 定义参数 ==========
// 产品
String[] products = {"P1", "P2"};
int numProducts = products.length;
// 时间段(天)
int numDays = 2;
// ========== 3. 创建求解器 ==========
// 使用 CBC 求解器(开源MIP求解器)
MPSolver solver = MPSolver.createSolver("CBC");
if (solver == null) {
System.err.println("无法创建 CBC 求解器,请检查 OR-Tools 依赖");
return;
}
// ========== 4. 定义决策变量 ==========
// x[i][t]: 产品i,第0-t天生产的数量(连续变量,≥0)
MPVariable[][] x = new MPVariable[numProducts][numDays];
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
x[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY, "x_" + products[i] + "_d" + (t + 1));
}
}
// I[i][t]: 产品i第0-t天末的库存量(连续变量,≥0)
MPVariable[][] inventory = new MPVariable[numProducts][numDays];
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
inventory[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY, "I_" + products[i] + "_d" + (t + 1));
}
}
// y[i][t]: 产品i,0-t天是否生产(0-1布尔变量)
MPVariable[][] y = new MPVariable[numProducts][numDays];
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
y[i][t] = solver.makeBoolVar("y_" + products[i] + "_d" + (t + 1));
}
}
// ========== 5. 目标函数:最小化总成本 ==========
MPObjective objective = solver.objective();
// 单位生产成本 c[i](元/件)
double[] unitCost = {5, 8};
// 生产成本
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
objective.setCoefficient(x[i][t], unitCost[i]);
}
}
// 单位库存持有成本 h[i](元/件/天)
double[] holdingCost = {0.5, 1.0};
// 库存持有成本
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
objective.setCoefficient(inventory[i][t], holdingCost[i]);
}
}
// 换型成本 s[i](元/次)
double[] setupCost = {100, 150};
// 换型成本
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
objective.setCoefficient(y[i][t], setupCost[i]);
}
}
objective.setMinimization();
// ========== 6. 添加约束 ==========
// 需求量 D[i][t]:产品i在第t天的需求
double[][] demand = {
{80, 60}, // P1: 第1天80, 第2天60
{50, 70} // P2: 第1天50, 第2天70
};
// 初始库存
double[] initialInventory = {20, 10};
// ---- 约束1:库存平衡 ----
// 第1天:初始库存 + 当天产量 = 当天需求 + 期末库存
for (int i = 0; i < numProducts; i++) {
MPConstraint invDay1 = solver.makeConstraint(demand[i][0], demand[i][0], "inv_balance_" + products[i] + "_d1");
invDay1.setCoefficient(x[i][0], 1);//当天产量*1
invDay1.setCoefficient(inventory[i][0], -1);//-期末库存*1
// x[i][0]−inventory[i][0]
// 移项后: x - I = D - I0 sum(当天产量-期末库存)
// 当天需求-初始库存 上下限
//上下界相等 → 等式约束
//当天产量-期末库存=当天需求-初始库存
//x[i][0]-inventory[i][0]=demand[i][0] - initialInventory[i]
//期末库存=当天产量-(当天需求-初始库存)
//当天产量=当天需求-初始库存+期末库存
//第 0 天期末库存 0,0 = 当天产量-当天需求(80,50)+初始库存(20,10)
//当天产量=60,40
//第 0 天期末库存 10,10 = 当天产量-当天需求(80,50)+初始库存(20,10)
//当天产量=70,50
invDay1.setBounds(0, demand[i][0] - initialInventory[i]);
}
// 第2天及以后:上期库存 + 当天产量 = 当天需求 + 期末库存
for (int i = 0; i < numProducts; i++) {
for (int t = 1; t < numDays; t++) {
MPConstraint inv = solver.makeConstraint(0, demand[i][t], "inv_balance_" + products[i] + "_d" + (t + 1));
inv.setCoefficient(inventory[i][t - 1], 1); // 上期库存
inv.setCoefficient(x[i][t], 1); // 当天产量
inv.setCoefficient(inventory[i][t], -1); // 期末库存
//上期库存+当天产量-期末库存=当天需求
//上期库存(0,0)+当天产量-当天需求(60,70)=期末库存
//当天产量=当天需求(60,70)-上期库存(0,0)+期末库存(0,0)
//上期库存(10,10)+当天产量-当天需求(60,70)=期末库存(10,10)
//当天产量=当天需求(60,70)-上期库存(10,10)+期末库存(10,10)
}
}
// ---- 约束2:安全库存约束 ----
// 安全库存
double safetyStock = 10;
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
MPConstraint ss = solver.makeConstraint(safetyStock, Double.POSITIVE_INFINITY, "safety_stock_" + products[i] + "_d" + (t + 1));
ss.setCoefficient(inventory[i][t], 1);
// 期末库存=安全库存
}
}
// ---- 约束3:产能约束 ----
// 生产效率 p[i](件/小时)
double[] productivity = {20, 10};
// 每日产能(小时)
double dailyCapacity = 10;
// 各产品产量/效率之和 ≤ 日产能
for (int t = 0; t < numDays; t++) {
MPConstraint cap = solver.makeConstraint(0, dailyCapacity, "capacity_d" + (t + 1));
for (int i = 0; i < numProducts; i++) {
//当天产量*1件需要时间<=每日产能
cap.setCoefficient(x[i][t], 1.0 / productivity[i]);
//一天最多干多少个
//当天产量<=生产效率*每日产能
//当天产量<=20*10,10*10
// 产品1 最多生产 200个
//产品2 最多生产 100个
}
}
// 大M常数(足够大的数,用于0-1开关约束)
double bigM = 10000;
// ---- 约束4:生产开关约束(大M法)----
// x[i][t] <= M * y[i][t] → x - M*y <= 0
//M*y=0,时x必须=0
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
MPConstraint switchCon = solver.makeConstraint(-Double.POSITIVE_INFINITY, 0, "switch_" + products[i] + "_d" + (t + 1));
switchCon.setCoefficient(x[i][t], 1);
switchCon.setCoefficient(y[i][t], -bigM);
}
}
// ---- 约束5:产线互斥(一天只能生产一种产品,可选)----
// 注:如果允许一天内换型多次,可移除此约束
for (int t = 0; t < numDays; t++) {
MPConstraint mutex = solver.makeConstraint(0, 1, "mutex_d" + (t + 1));
for (int i = 0; i < numProducts; i++) {
mutex.setCoefficient(y[i][t], 1);
//y[1][1]+y[2][1]<=1; 每天只能生产一个商品,y[1][1]和y[2][1]不能同时为1
//y[1][2]+y[2][2]<=1;
}
//约束5--影响y>约束4---影响y--影响x> x=0的话约束1不成立 初始库存(20) + 当天产量(0) = 当天需求(80) + 期末库存(10) 不成立无解
}
// ========== 7. 求解 ==========
System.out.println("========== 开始求解 ==========");
MPSolver.ResultStatus status = solver.solve();
// ========== 8. 输出结果 ==========
if (status == MPSolver.ResultStatus.OPTIMAL) {
System.out.println("✅ 找到最优解!");
System.out.printf("最小总成本:%.2f 元%n", objective.value());
System.out.println();
// 按天输出排产结果
for (int t = 0; t < numDays; t++) {
System.out.println("━━━━━━━━ 第 " + (t + 1) + " 天 ━━━━━━━━");
double totalHours = 0;
for (int i = 0; i < numProducts; i++) {
double d= demand[i][t];
double qty = x[i][t].solutionValue();
double hours = qty / productivity[i];
totalHours += hours;
if (qty > 0) {
System.out.printf(" 生产 %s:%.0f 件,需求%.0f 件 耗时 %.1f 小时,换型成本 %.0f 元%n",
products[i], qty,d, hours, y[i][t].solutionValue() * setupCost[i]);
} else {
System.out.printf(" 生产 %s:休息(0件)%n", products[i]);
}
}
System.out.printf(" 当日总工时:%.1f / %.1f 小时%n", totalHours, dailyCapacity);
System.out.println(" --- 期末库存 ---");
for (int i = 0; i < numProducts; i++) {
System.out.printf(" %s 库存:%.0f 件%n", products[i], inventory[i][t].solutionValue());
}
System.out.println();
}
// 成本明细
double totalProdCost = 0, totalHoldCost = 0, totalSetupCost = 0;
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
totalProdCost += x[i][t].solutionValue() * unitCost[i];
totalHoldCost += inventory[i][t].solutionValue() * holdingCost[i];
totalSetupCost += y[i][t].solutionValue() * setupCost[i];
}
}
System.out.println("========== 成本明细 ==========");
System.out.printf("生产成本: %.2f 元%n", totalProdCost);
System.out.printf("库存成本: %.2f 元%n", totalHoldCost);
System.out.printf("换型成本: %.2f 元%n", totalSetupCost);
System.out.printf("合计: %.2f 元%n", objective.value());
} else if (status == MPSolver.ResultStatus.FEASIBLE) {
System.out.println("⚠️ 找到可行解,但非最优");
System.out.printf("当前成本:%.2f 元%n", objective.value());
} else {
System.out.println("❌ 无解或求解失败,状态:" + status);
}
MPModelExportOptions options=new MPModelExportOptions();
String lpText = solver.exportModelAsLpFormat(false);
FileHelper.writeFile(lpText,"model.lp");
for(MPVariable var : solver.variables()){
String name = var.name();
double val = var.solutionValue();
double lb = var.lb();
double ub = var.ub();
System.out.printf("变量[%s] 下界=%.2f 上界=%.2f 最优解=%.4f%n",
name, lb, ub, val);
}
// 求解统计
System.out.println();
System.out.println("========== 求解统计 ==========");
System.out.println("变量数量:" + solver.numVariables());
System.out.println("约束数量:" + solver.numConstraints());
System.out.printf("求解时间:%.2f 秒%n", solver.wallTime() / 1000.0);
}
}
package com.aps.service.mp;
import com.aps.common.util.FileHelper;
import com.aps.common.util.TeePrintStream;
import com.google.ortools.Loader;
import com.google.ortools.linearsolver.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
/**
* 作者:佟礼
* 时间:2026-07-23
* MIP 主生产排程 Demo
* 场景:1条产线,2种产品,2天排产
* 目标:最小化 生产成本 + 库存持有成本 + 换型成本
* 在MpsSchedulingDemo中发现两个问题,
* 1 约束5造成一天只能生产一个产品,约束1 产品必须生产,造成无解,2 需求数超过生产能力无解
*/
public class MpsSchedulingDemo2 {
// 关闭日志文件和恢复流
public static void main(String[] args) throws FileNotFoundException, UnsupportedEncodingException {
// 初始化日志文件
// 1. 加载 OR-Tools 本地库(必须)
Loader.loadNativeLibraries();
// ========== 2. 定义参数 ==========
// 产品
String[] products = {"P1", "P2"};
int numProducts = products.length;
// 保存原始控制台输出流
PrintStream originalOut = System.out;
PrintStream originalErr = System.err;
String logPath = "scip_full.log";
// 时间段(天)
int numDays = 2;
PrintStream logWriter = new PrintStream(
new FileOutputStream(logPath, false),
true,
StandardCharsets.UTF_8.name()
);
System.setOut(logWriter);
System.setErr(logWriter);
// ========== 3. 创建求解器 ==========
// 使用 CBC 求解器(开源MIP求解器)
// MPSolver solver = MPSolver.createSolver("CBC");
MPSolver solver = new MPSolver("demo",
MPSolver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING);
if (solver == null) {
System.err.println("无法创建 CBC 求解器,请检查 OR-Tools 依赖");
return;
}
String params = String.join(";",
"display/verblevel = 5", // 最高详细日志,输出每轮Gap
"display/logfile = solver.log",// 日志写入文件,控制台干净
"separating/maxrounds = 10", // 割平面迭代轮数
"limits/gap = 0.001" // 最优间隙阈值0.1%
);
solver.setSolverSpecificParametersAsString("display/verblevel = 5");
solver.setSolverSpecificParametersAsString("separating/maxrounds = 10");
solver.setSolverSpecificParametersAsString("limits/gap = 0.001");
solver.setSolverSpecificParametersAsString("limits/time = 300");
solver.enableOutput();
// ========== 4. 定义决策变量 ==========
// x[i][t]: 产品i,第0-t天生产的数量(连续变量,≥0)
MPVariable[][] x = new MPVariable[numProducts][numDays];
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
x[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY, "x_" + products[i] + "_d" + (t + 1));
}
}
// I[i][t]: 产品i第0-t天末的库存量(连续变量,≥0)
MPVariable[][] inventory = new MPVariable[numProducts][numDays];
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
inventory[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY, "I_" + products[i] + "_d" + (t + 1));
}
}
// shortfall[i][t]: 产品i第t天未满足的需求量(≥0),解决互斥约束下某产品产量=0时等式不成立的问题
MPVariable[][] shortfall = new MPVariable[numProducts][numDays];
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
shortfall[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY, "short_" + products[i] + "_d" + (t + 1));
}
}
// y[i][t]: 产品i,0-t天是否生产(0-1布尔变量)
MPVariable[][] y = new MPVariable[numProducts][numDays];
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
y[i][t] = solver.makeBoolVar("y_" + products[i] + "_d" + (t + 1));
}
}
// ========== 5. 目标函数:最小化总成本 ==========
MPObjective objective = solver.objective();
// 单位生产成本 c[i](元/件)
double[] unitCost = {5, 8};
// 生产成本
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
objective.setCoefficient(x[i][t], unitCost[i]);
}
}
// 单位库存持有成本 h[i](元/件/天)
double[] holdingCost = {0.5, 1.0};
// 库存持有成本
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
objective.setCoefficient(inventory[i][t], holdingCost[i]);
}
}
// 换型成本 s[i](元/次)
double[] setupCost = {100, 150};
// 换型成本
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
objective.setCoefficient(y[i][t], setupCost[i]);
}
}
// 缺口惩罚(元/件),权重最大,优先满足需求
double shortfallPenalty = 1000;
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
objective.setCoefficient(shortfall[i][t], shortfallPenalty);
}
}
objective.setMinimization();
// ========== 6. 添加约束 ==========
// 需求量 D[i][t]:产品i在第t天的需求
double[][] demand = {
{80, 60}, // P1: 第1天80, 第2天60
{50, 70} // P2: 第1天50, 第2天70
};
// 初始库存
double[] initialInventory = {20, 10};
// ---- 约束1:库存平衡 ----
// 第1天:初始库存 + 当天产量 = 当天需求 + 期末库存,当天产量-期末库存=当天需求-初始库存
// ---- 约束1:库存平衡(加入shortfall吸收缺口)----
// 公式:x + shortfall − inventory = demand − startInventory
// 第1天:startInventory = initialInventory
for (int i = 0; i < numProducts; i++) {
// MPConstraint invDay1 = solver.makeConstraint(demand[i][0], demand[i][0], "inv_balance_" + products[i] + "_d1");
// invDay1.setCoefficient(x[i][0], 1);//当天产量*1
// invDay1.setCoefficient(inventory[i][0], -1);//-期末库存*1
// x[i][0]−inventory[i][0]
// 移项后: x - I = D - I0 sum(当天产量-期末库存)
// 当天需求-初始库存 上下限
//上下界相等 → 等式约束
//当天产量-期末库存=当天需求-初始库存
//x[i][0]-inventory[i][0]=demand[i][0] - initialInventory[i]
//期末库存=当天产量-(当天需求-初始库存)
//当天产量=当天需求-初始库存+期末库存
//第 0 天期末库存 0,0 = 当天产量-当天需求(80,50)+初始库存(20,10)
//当天产量=60,40
//第 0 天期末库存 10,10 = 当天产量-当天需求(80,50)+初始库存(20,10)
//当天产量=70,50
// invDay1.setBounds(0, demand[i][0] - initialInventory[i]);
// x + shortfall − inventory = demand − startInventory
//P1 130+0-70=80-20
//P2 0+50-10 =50-10
MPConstraint invDay1 = solver.makeConstraint(
demand[i][0] - initialInventory[i],
demand[i][0] - initialInventory[i],
"inv_balance_" + products[i] + "_d1");
invDay1.setCoefficient(x[i][0], 1);
invDay1.setCoefficient(shortfall[i][0], 1); // 缺口补等式
invDay1.setCoefficient(inventory[i][0], -1);
}
// 第2天及以后:上期库存 + 当天产量 = 当天需求 + 期末库存
// 第2天及以后:startInventory = 上期期末库存
// 公式:inventory[i][t-1] + x[i][t] + shortfall[i][t] = demand[i][t] + inventory[i][t]
// 移项后:inventory[i][t-1] + x[i][t] + shortfall[i][t] - inventory[i][t] = demand[i][t]
for (int i = 0; i < numProducts; i++) {
for (int t = 1; t < numDays; t++) {
// MPConstraint inv = solver.makeConstraint(0, demand[i][t], "inv_balance_" + products[i] + "_d" + (t + 1));
// inv.setCoefficient(inventory[i][t - 1], 1); // 上期库存
// inv.setCoefficient(x[i][t], 1); // 当天产量
// inv.setCoefficient(inventory[i][t], -1); // 期末库存
//上期库存+当天产量-期末库存=当天需求
//上期库存(0,0)+当天产量-当天需求(60,70)=期末库存
//当天产量=当天需求(60,70)-上期库存(0,0)+期末库存(0,0)
//上期库存(10,10)+当天产量-当天需求(60,70)=期末库存(10,10)
//当天产量=当天需求(60,70)-上期库存(10,10)+期末库存(10,10)
// 公式:inventory[i][t-1] + x[i][t] + shortfall[i][t] = demand[i][t] + inventory[i][t]
//P1 0+0-10=60-70
//P2 70+0-10 =70-10
MPConstraint inv = solver.makeConstraint(
demand[i][t], demand[i][t],
"inv_balance_" + products[i] + "_d" + (t + 1));
inv.setCoefficient(inventory[i][t - 1], 1); // 上期库存
inv.setCoefficient(x[i][t], 1); // 当天产量
inv.setCoefficient(shortfall[i][t], 1); // 缺口补等式
inv.setCoefficient(inventory[i][t], -1); // 期末库存
}
}
// ---- 约束2:安全库存约束 ----
// 安全库存
double safetyStock = 10;
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
MPConstraint ss = solver.makeConstraint(safetyStock, Double.POSITIVE_INFINITY, "safety_stock_" + products[i] + "_d" + (t + 1));
ss.setCoefficient(inventory[i][t], 1);
// 期末库存=安全库存
}
}
// ---- 约束3:产能约束 ----
// 生产效率 p[i](件/小时)
double[] productivity = {20, 10};
// 每日产能(小时)
double dailyCapacity = 10;
// 各产品产量/效率之和 ≤ 日产能
for (int t = 0; t < numDays; t++) {
MPConstraint cap = solver.makeConstraint(0, dailyCapacity, "capacity_d" + (t + 1));
for (int i = 0; i < numProducts; i++) {
//当天产量*1件需要时间<=每日产能
cap.setCoefficient(x[i][t], 1.0 / productivity[i]);
//一天最多干多少个
//当天产量<=生产效率*每日产能
//当天产量<=20*10,10*10
// 产品1 最多生产 200个
//产品2 最多生产 100个
}
}
// 大M常数(足够大的数,用于0-1开关约束)
double bigM = 10000;
// ---- 约束4:生产开关约束(大M法)----
// x[i][t] <= M * y[i][t] → x - M*y <= 0
//M*y=0,时x必须=0
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
MPConstraint switchCon = solver.makeConstraint(-Double.POSITIVE_INFINITY, 0, "switch_" + products[i] + "_d" + (t + 1));
switchCon.setCoefficient(x[i][t], 1);
switchCon.setCoefficient(y[i][t], -bigM);
}
}
// ---- 约束5:产线互斥(一天只能生产一种产品,可选)----
// 注:如果允许一天内换型多次,可移除此约束
for (int t = 0; t < numDays; t++) {
MPConstraint mutex = solver.makeConstraint(0, 1, "mutex_d" + (t + 1));
for (int i = 0; i < numProducts; i++) {
mutex.setCoefficient(y[i][t], 1);
//y[1][1]+y[2][1]<=1; 每天只能生产一个商品,y[1][1]和y[2][1]不能同时为1
//y[1][2]+y[2][2]<=1;
}
//约束5--影响y>约束4---影响y--影响x> x=0的话约束1不成立 初始库存(20) + 当天产量(0) = 当天需求(80) + 期末库存(10) 不成立无解
}
// ========== 7. 求解 ==========
System.out.println("========== 开始求解 ==========");
MPSolver.ResultStatus status = solver.solve();
// ========== 8. 输出结果 ==========
if (status == MPSolver.ResultStatus.OPTIMAL) {
System.out.println("✅ 找到最优解!");
System.out.printf("最小总成本:%.2f 元%n", objective.value());
System.out.println();
// 按天输出排产结果
for (int t = 0; t < numDays; t++) {
System.out.println("━━━━━━━━ 第 " + (t + 1) + " 天 ━━━━━━━━");
double totalHours = 0;
for (int i = 0; i < numProducts; i++) {
double d = demand[i][t];
double qty = x[i][t].solutionValue();
double sht = shortfall[i][t].solutionValue();
double hours = qty / productivity[i];
totalHours += hours;
if (qty > 0) {
System.out.printf(" 生产 %s:%.0f 件,需求 %.0f 件,耗时 %.1f 小时,换型成本 %.0f 元%n",
products[i], qty, d, hours, y[i][t].solutionValue() * setupCost[i]);
} else {
System.out.printf(" 生产 %s:休息(0件)%n", products[i]);
}
if (sht > 0.001) {
System.out.printf(" ⚠ 未满足需求(shortfall):%.0f 件%n", sht);
}
}
System.out.printf(" 当日总工时:%.1f / %.1f 小时%n", totalHours, dailyCapacity);
System.out.println(" --- 期末库存 ---");
for (int i = 0; i < numProducts; i++) {
System.out.printf(" %s 库存:%.0f 件%n", products[i], inventory[i][t].solutionValue());
}
System.out.println();
}
// 成本明细
double totalProdCost = 0, totalHoldCost = 0, totalSetupCost = 0, totalShortCost = 0;
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
totalProdCost += x[i][t].solutionValue() * unitCost[i];
totalHoldCost += inventory[i][t].solutionValue() * holdingCost[i];
totalSetupCost += y[i][t].solutionValue() * setupCost[i];
totalShortCost += shortfall[i][t].solutionValue() * shortfallPenalty;
}
}
System.out.println("========== 成本明细 ==========");
System.out.printf("生产成本: %.2f 元%n", totalProdCost);
System.out.printf("库存成本: %.2f 元%n", totalHoldCost);
System.out.printf("换型成本: %.2f 元%n", totalSetupCost);
System.out.printf("缺口惩罚: %.2f 元%n", totalShortCost);
System.out.printf("合计: %.2f 元%n", objective.value());
} else if (status == MPSolver.ResultStatus.FEASIBLE) {
System.out.println("⚠️ 找到可行解,但非最优");
System.out.printf("当前成本:%.2f 元%n", objective.value());
} else {
System.out.println("❌ 无解或求解失败,状态:" + status);
}
MPModelExportOptions options=new MPModelExportOptions();
String lpText = solver.exportModelAsLpFormat(false);
FileHelper.writeFile(lpText,"model.lp");
String mpsText = solver.exportModelAsMpsFormat(true,true);
FileHelper.writeFile(mpsText,"model.mps");
// 输出LP模型到控制台
System.out.println("========== LP模型输出 ==========");
// System.out.println(lpText);
for(MPVariable var : solver.variables()){
String name = var.name();
double val = var.solutionValue();
double lb = var.lb();
double ub = var.ub();
System.out.printf("变量[%s] 下界=%.2f 上界=%.2f 最优解=%.4f%n",
name, lb, ub, val);
}
// 求解统计
System.out.println();
System.out.println("========== 求解统计 ==========");
System.out.println("变量数量:" + solver.numVariables());
System.out.println("约束数量:" + solver.numConstraints());
System.out.printf("求解时间:%.2f 秒%n", solver.wallTime() / 1000.0);
}
}
package com.aps.service.mp;
import com.aps.common.util.FileHelper;
import com.google.ortools.Loader;
import com.google.ortools.linearsolver.*;
/**
* 作者:佟礼
* 时间:2026-07-23
* MIP 主生产排程 Demo
* 场景:1条产线,2种产品,2天排产
* 目标:最小化 生产成本 + 库存持有成本 + 换型成本
* 在MpsSchedulingDemo中发现两个问题,
* 1 约束5造成一天只能生产一个产品,约束1 产品必须生产,造成无解,2 需求数超过生产能力无解
*/
public class MpsSchedulingDemo22 {
public static void main(String[] args) {
// 1. 加载 OR-Tools 本地库(必须)
Loader.loadNativeLibraries();
// ========== 2. 定义参数 ==========
// 产品
String[] products = {"P1", "P2"};
int numProducts = products.length;
// 时间段(天)
int numDays = 2;
// ========== 3. 创建求解器 ==========
// 使用 CBC 求解器(开源MIP求解器)
MPSolver solver = MPSolver.createSolver("CBC");
if (solver == null) {
System.err.println("无法创建 CBC 求解器,请检查 OR-Tools 依赖");
return;
}
// ========== 4. 定义决策变量 ==========
// x[i][t]: 产品i,第0-t天生产的数量(连续变量,≥0)
MPVariable[][] x = new MPVariable[numProducts][numDays];
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
x[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY, "x_" + products[i] + "_d" + (t + 1));
}
}
// I[i][t]: 产品i第0-t天末的库存量(连续变量,≥0)
MPVariable[][] inventory = new MPVariable[numProducts][numDays];
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
inventory[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY, "I_" + products[i] + "_d" + (t + 1));
}
}
// shortfall[i][t]: 产品i第t天未满足的需求量(≥0),解决互斥约束下某产品产量=0时等式不成立的问题
MPVariable[][] shortfall = new MPVariable[numProducts][numDays];
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
shortfall[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY, "short_" + products[i] + "_d" + (t + 1));
}
}
// y[i][t]: 产品i,0-t天是否生产(0-1布尔变量)
MPVariable[][] y = new MPVariable[numProducts][numDays];
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
y[i][t] = solver.makeBoolVar("y_" + products[i] + "_d" + (t + 1));
}
}
// ========== 5. 目标函数:最小化总成本 ==========
MPObjective objective = solver.objective();
// 单位生产成本 c[i](元/件)
double[] unitCost = {5, 8};
// 生产成本
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
objective.setCoefficient(x[i][t], unitCost[i]);
}
}
// 单位库存持有成本 h[i](元/件/天)
double[] holdingCost = {0.5, 1.0};
// 库存持有成本
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
objective.setCoefficient(inventory[i][t], holdingCost[i]);
}
}
// 换型成本 s[i](元/次)
double[] setupCost = {100, 150};
// 换型成本
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
objective.setCoefficient(y[i][t], setupCost[i]);
}
}
// 缺口惩罚(元/件),权重最大,优先满足需求
double shortfallPenalty = 1000;
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
objective.setCoefficient(shortfall[i][t], shortfallPenalty);
}
}
objective.setMinimization();
// ========== 6. 添加约束 ==========
// 需求量 D[i][t]:产品i在第t天的需求
double[][] demand = {
{80, 60}, // P1: 第1天80, 第2天60
{50, 70} // P2: 第1天50, 第2天70
};
// 初始库存
double[] initialInventory = {20, 10};
// ---- 约束1:库存平衡 ----
// 第1天:初始库存 + 当天产量 = 当天需求 + 期末库存
for (int i = 0; i < numProducts; i++) {
MPConstraint invDay1 = solver.makeConstraint(demand[i][0], demand[i][0], "inv_balance_" + products[i] + "_d1");
invDay1.setCoefficient(x[i][0], 1);//当天产量*1
invDay1.setCoefficient(inventory[i][0], -1);//-期末库存*1
// x[i][0]−inventory[i][0]
// 移项后: x - I = D - I0 sum(当天产量-期末库存)
// 当天需求-初始库存 上下限
//上下界相等 → 等式约束
//当天产量-期末库存=当天需求-初始库存
//x[i][0]-inventory[i][0]=demand[i][0] - initialInventory[i]
//期末库存=当天产量-(当天需求-初始库存)
//当天产量=当天需求-初始库存+期末库存
//第 0 天期末库存 0,0 = 当天产量-当天需求(80,50)+初始库存(20,10)
//当天产量=60,40
//第 0 天期末库存 10,10 = 当天产量-当天需求(80,50)+初始库存(20,10)
//当天产量=70,50
invDay1.setBounds(0, demand[i][0] - initialInventory[i]);
}
// 第2天及以后:上期库存 + 当天产量 = 当天需求 + 期末库存
for (int i = 0; i < numProducts; i++) {
for (int t = 1; t < numDays; t++) {
MPConstraint inv = solver.makeConstraint(0, demand[i][t], "inv_balance_" + products[i] + "_d" + (t + 1));
inv.setCoefficient(inventory[i][t - 1], 1); // 上期库存
inv.setCoefficient(x[i][t], 1); // 当天产量
inv.setCoefficient(inventory[i][t], -1); // 期末库存
//上期库存+当天产量-期末库存=当天需求
//上期库存(0,0)+当天产量-当天需求(60,70)=期末库存
//当天产量=当天需求(60,70)-上期库存(0,0)+期末库存(0,0)
//上期库存(10,10)+当天产量-当天需求(60,70)=期末库存(10,10)
//当天产量=当天需求(60,70)-上期库存(10,10)+期末库存(10,10)
}
}
// ---- 约束2:安全库存约束 ----
// 安全库存
double safetyStock = 10;
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
MPConstraint ss = solver.makeConstraint(safetyStock, Double.POSITIVE_INFINITY, "safety_stock_" + products[i] + "_d" + (t + 1));
ss.setCoefficient(inventory[i][t], 1);
// 期末库存=安全库存
}
}
// ---- 约束3:产能约束 ----
// 生产效率 p[i](件/小时)
double[] productivity = {20, 10};
// 每日产能(小时)
double dailyCapacity = 10;
// 各产品产量/效率之和 ≤ 日产能
for (int t = 0; t < numDays; t++) {
MPConstraint cap = solver.makeConstraint(0, dailyCapacity, "capacity_d" + (t + 1));
for (int i = 0; i < numProducts; i++) {
//当天产量*1件需要时间<=每日产能
cap.setCoefficient(x[i][t], 1.0 / productivity[i]);
//一天最多干多少个
//当天产量<=生产效率*每日产能
//当天产量<=20*10,10*10
// 产品1 最多生产 200个
//产品2 最多生产 100个
}
}
// 大M常数(足够大的数,用于0-1开关约束)
double bigM = 10000;
// ---- 约束4:生产开关约束(大M法)----
// x[i][t] <= M * y[i][t] → x - M*y <= 0
//M*y=0,时x必须=0
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
MPConstraint switchCon = solver.makeConstraint(-Double.POSITIVE_INFINITY, 0, "switch_" + products[i] + "_d" + (t + 1));
switchCon.setCoefficient(x[i][t], 1);
switchCon.setCoefficient(y[i][t], -bigM);
}
}
// ---- 约束5:产线互斥(一天只能生产一种产品,可选)----
// 注:如果允许一天内换型多次,可移除此约束
for (int t = 0; t < numDays; t++) {
MPConstraint mutex = solver.makeConstraint(0, 1, "mutex_d" + (t + 1));
for (int i = 0; i < numProducts; i++) {
mutex.setCoefficient(y[i][t], 1);
//y[1][1]+y[2][1]<=1; 每天只能生产一个商品,y[1][1]和y[2][1]不能同时为1
//y[1][2]+y[2][2]<=1;
}
//约束5--影响y>约束4---影响y--影响x> x=0的话约束1不成立 初始库存(20) + 当天产量(0) = 当天需求(80) + 期末库存(10) 不成立无解
}
// ========== 7. 求解 ==========
System.out.println("========== 开始求解 ==========");
MPSolver.ResultStatus status = solver.solve();
// ========== 8. 输出结果 ==========
if (status == MPSolver.ResultStatus.OPTIMAL) {
System.out.println("✅ 找到最优解!");
System.out.printf("最小总成本:%.2f 元%n", objective.value());
System.out.println();
// 按天输出排产结果
for (int t = 0; t < numDays; t++) {
System.out.println("━━━━━━━━ 第 " + (t + 1) + " 天 ━━━━━━━━");
double totalHours = 0;
for (int i = 0; i < numProducts; i++) {
double d= demand[i][t];
double qty = x[i][t].solutionValue();
double hours = qty / productivity[i];
totalHours += hours;
if (qty > 0) {
System.out.printf(" 生产 %s:%.0f 件,需求%.0f 件 耗时 %.1f 小时,换型成本 %.0f 元%n",
products[i], qty,d, hours, y[i][t].solutionValue() * setupCost[i]);
} else {
System.out.printf(" 生产 %s:休息(0件)%n", products[i]);
}
}
System.out.printf(" 当日总工时:%.1f / %.1f 小时%n", totalHours, dailyCapacity);
System.out.println(" --- 期末库存 ---");
for (int i = 0; i < numProducts; i++) {
System.out.printf(" %s 库存:%.0f 件%n", products[i], inventory[i][t].solutionValue());
}
System.out.println();
}
// 成本明细
double totalProdCost = 0, totalHoldCost = 0, totalSetupCost = 0;
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
totalProdCost += x[i][t].solutionValue() * unitCost[i];
totalHoldCost += inventory[i][t].solutionValue() * holdingCost[i];
totalSetupCost += y[i][t].solutionValue() * setupCost[i];
}
}
System.out.println("========== 成本明细 ==========");
System.out.printf("生产成本: %.2f 元%n", totalProdCost);
System.out.printf("库存成本: %.2f 元%n", totalHoldCost);
System.out.printf("换型成本: %.2f 元%n", totalSetupCost);
System.out.printf("合计: %.2f 元%n", objective.value());
} else if (status == MPSolver.ResultStatus.FEASIBLE) {
System.out.println("⚠️ 找到可行解,但非最优");
System.out.printf("当前成本:%.2f 元%n", objective.value());
} else {
System.out.println("❌ 无解或求解失败,状态:" + status);
}
MPModelExportOptions options=new MPModelExportOptions();
String lpText = solver.exportModelAsLpFormat(true);
FileHelper.writeFile(lpText,"model.lp");
for(MPVariable var : solver.variables()){
String name = var.name();
double val = var.solutionValue();
double lb = var.lb();
double ub = var.ub();
System.out.printf("变量[%s] 下界=%.2f 上界=%.2f 最优解=%.4f%n",
name, lb, ub, val);
}
// 求解统计
System.out.println();
System.out.println("========== 求解统计 ==========");
System.out.println("变量数量:" + solver.numVariables());
System.out.println("约束数量:" + solver.numConstraints());
System.out.printf("求解时间:%.2f 秒%n", solver.wallTime() / 1000.0);
}
}
package com.aps.service.mp;
import com.aps.common.util.FileHelper;
import com.google.ortools.Loader;
import com.google.ortools.linearsolver.*;
import java.io.*;
/**
* 作者:佟礼
* 时间:2026-07-23
* MIP 主生产排程 Demo
* 多产线 MIP 主生产排程
* 场景:2条产线(L1, L2),3种产品(P1, P2, P3),2天排产
* 目标:最小化 生产成本 + 库存持有成本 + 换型成本
* 特性:每条产线效率/成本不同,模型自动选择最优产线分配
* 按生产顺序计算换型成本:支持任意数量产品的切换
*/
public class MultiLineMpsScheduling {
//**产线分配**:P1 放在 L1 还是 L2?—— 比较 `成本差` vs `效率差带来的工时影响`
// **生产批量**:一天生产完还是分两天生产?—— 比较 `换型成本` vs `库存持有成本`
// **负载均衡**:两条产线谁多干谁少干?—— 在满足交期前提下,优先用单位成本最低的产线
// **换型权衡**:一天内要不要换型生产两种产品?—— 比较 `换型费` vs `多一天的库存费`
public static void main(String[] args) throws FileNotFoundException, UnsupportedEncodingException {
// 1. 加载 OR-Tools 本地库(必须)
Loader.loadNativeLibraries();
// ========== 2. 定义参数 ==========
// 产品
String[] products = {"P1", "P2", "P3"};
int numProducts = products.length;
// 每天最多生产的产品数量(用于定义位置变量的维度)
int maxProductsPerDay = numProducts;
// 产线
String[] lines = {"L1", "L2"};
int numLines = lines.length;
// 时间段(天)
int numDays = 2;
// ========== 3. 创建求解器 ==========
// 使用 CBC 求解器(开源MIP求解器)
// MPSolver solver = MPSolver.createSolver("CBC");
MPSolver solver = new MPSolver("demo",
MPSolver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING);
if (solver == null) {
System.err.println("无法创建求解器,请检查 OR-Tools 依赖");
return;
}
solver.enableOutput();
// ========== 4. 定义决策变量 ==========
// x[i][j][t]: 产品i,在j产线,第0-t天生产的数量(连续变量,≥0)
MPVariable[][][] x = new MPVariable[numProducts][numLines][numDays];
for (int i = 0; i < numProducts; i++) {
for (int j = 0; j < numLines; j++) {
for (int t = 0; t < numDays; t++) {
x[i][j][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
"x_" + products[i] + "_" + lines[j] + "_d" + (t + 1));
}
}
}
// I[i][t]: 产品i第0-t天末的库存量(连续变量,≥0)
MPVariable[][] inventory = new MPVariable[numProducts][numDays];
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
inventory[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY, "I_" + products[i] + "_d" + (t + 1));
}
}
// y[i][j][t]: 第t天产线j是否生产产品i(0-1变量)
MPVariable[][][] y = new MPVariable[numProducts][numLines][numDays];
for (int i = 0; i < numProducts; i++) {
for (int j = 0; j < numLines; j++) {
for (int t = 0; t < numDays; t++) {
y[i][j][t] = solver.makeBoolVar(
"y_" + products[i] + "_" + lines[j] + "_d" + (t + 1));
}
}
}
// shortfall[i][t]: 产品i第t天未满足的需求量(≥0),解决互斥约束下某产品产量=0时等式不成立的问题
MPVariable[][] shortfall = new MPVariable[numProducts][numDays];
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
shortfall[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY, "short_" + products[i] + "_d" + (t + 1));
}
}
// ========== 换型相关变量(支持任意数量产品)==========
// s[i][k][j][t]: 产线j第t天的第k个位置是否生产产品i(0-1变量)
// k=0表示第一个位置,k=1表示第二个位置,以此类推
MPVariable[][][][] s = new MPVariable[numProducts][maxProductsPerDay][numLines][numDays];
for (int i = 0; i < numProducts; i++) {
for (int k = 0; k < maxProductsPerDay; k++) {
for (int j = 0; j < numLines; j++) {
for (int t = 0; t < numDays; t++) {
s[i][k][j][t] = solver.makeBoolVar(
"s_" + products[i] + "_pos" + (k + 1) + "_" + lines[j] + "_d" + (t + 1));
//s_P1_pos1_L1_d1,s_P1_pos1_L1_d2
//s_P1_pos2_L1_d1,s_P1_pos2_L1_d2
//s_P1_pos3_L1_d1,s_P1_pos3_L1_d2
//s_P2_pos1_L1_d1,s_P2_pos1_L1_d2
//s_P2_pos2_L1_d1,s_P2_pos2_L1_d2
//s_P2_pos3_L1_d1,s_P2_pos3_L1_d2
//s_P3_pos1_L1_d1,s_P3_pos1_L1_d2
//s_P3_pos2_L1_d1,s_P3_pos2_L1_d2
//s_P3_pos3_L1_d1,s_P3_pos3_L1_d2
//s_P1_pos2_L2_d1,s_P1_pos2_L2_d2
//s_P1_pos3_L2_d1,s_P1_pos3_L2_d2
}
}
}
}
// lastProduct[i][j][t]: 产线j第t天最后生产的产品是否是i(0-1变量)
// 只有当产品i在某个位置k,且位置k+1没有产品时,lastProduct[i][j][t] = 1
MPVariable[][][] lastProduct = new MPVariable[numProducts][numLines][numDays];
for (int i = 0; i < numProducts; i++) {
for (int j = 0; j < numLines; j++) {
for (int t = 0; t < numDays; t++) {
lastProduct[i][j][t] = solver.makeBoolVar(
"last_" + products[i] + "_" + lines[j] + "_d" + (t + 1));
//last_P1_L1_d1,last_P1_L1_d2
//last_P2_L1_d1,last_P2_L1_d2
//last_P3_L1_d1,last_P3_L1_d2
}
}
}
// switchTo[i][j][t]: 产线j第t天是否切换到产品i(包括同一天切换和跨天切换)(0-1变量)
MPVariable[][][] switchTo = new MPVariable[numProducts][numLines][numDays];
for (int i = 0; i < numProducts; i++) {
for (int j = 0; j < numLines; j++) {
for (int t = 0; t < numDays; t++) {
switchTo[i][j][t] = solver.makeBoolVar(
"switchTo_" + products[i] + "_" + lines[j] + "_d" + (t + 1));
//switchTo_P1_L1_d1,switchTo_P1_L1_d2
//switchTo_P2_L1_d1,switchTo_P2_L1_d2
//switchTo_P3_L1_d1,switchTo_P3_L1_d2
}
}
}
// ========== 5. 目标函数:最小化总成本 ==========
MPObjective objective = solver.objective();
// 单位生产成本 c[i][j](元/件)
double[][] unitCost = {
{5.0, 4.0}, // P1: L1=5元, L2=4元
{8.0, 7.0}, // P2: L1=8元, L2=7元
{6.0, 5.0} // P3: L1=6元, L2=5元
}; // 生产成本
// 生产成本
for (int i = 0; i < numProducts; i++) {
for (int j = 0; j < numLines; j++) {
for (int t = 0; t < numDays; t++) {
objective.setCoefficient(x[i][j][t], unitCost[i][j]);
}
}
}
// 单位库存持有成本 h[i](元/件/天)
double[] holdingCost = {0.5, 1.0, 0.8};
// 库存持有成本
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
objective.setCoefficient(inventory[i][t], holdingCost[i]);
}
}
// ---- 换型成本 s[i][j]:产线j切换到产品i的换型成本(元/次)----
double[][] setupCost = {
{100, 80}, // P1: L1换型100元, L2换型80元
{150, 120},// P2: L1换型150元, L2换型120元
{120, 90} // P3: L1换型120元, L2换型90元
}; // 换型成本
// 换型成本(每条产线分别计算)
// 只有从一种产品切换到另一种产品时才产生换型成本
// 换型成本:只有切换到产品i时才产生换型成本
for (int i = 0; i < numProducts; i++) {
for (int j = 0; j < numLines; j++) {
for (int t = 0; t < numDays; t++) {
objective.setCoefficient(switchTo[i][j][t], setupCost[i][j]);
}
}
}
// 缺口惩罚(元/件),权重最大,优先满足需求
double shortfallPenalty = 1000;
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
objective.setCoefficient(shortfall[i][t], shortfallPenalty);
}
}
objective.setMinimization();
// ========== 6. 添加约束 ==========
// 需求量 D[i][t]:产品i在第t天的需求
double[][] demand = {
{80, 60}, // P1: 第1天80, 第2天60
{50, 70}, // P2: 第1天50, 第2天70
{30, 40} // P3: 第1天30, 第2天40
};
// 初始库存
double[] initialInventory = {20, 10, 5};
// ---- 约束1:库存平衡(加入shortfall吸收缺口)----
// 公式:x + shortfall − inventory = demand − startInventory
// 第1天:startInventory = initialInventory
//上下界相等 → 等式约束
// 第1天:各个产线当天产量+产品缺口-期末库存=当天需求-初始库存
//当天产量-期末库存=当天需求-初始库存
//当天产量=当天需求-初始库存+期末库存
for (int i = 0; i < numProducts; i++) {
MPConstraint invDay1 = solver.makeConstraint(
demand[i][0] - initialInventory[i],
demand[i][0] - initialInventory[i],
"inv_balance_" + products[i] + "_d1");
for (int j = 0; j < numLines; j++) {
invDay1.setCoefficient(x[i][j][0], 1);
}
invDay1.setCoefficient(shortfall[i][0], 1); // 缺口补等式
invDay1.setCoefficient(inventory[i][0], -1);
}
// 第2天及以后:上期库存 + 各产线当天产量的和 + 缺口 = 当天需求 + 期末库存
// 第2天及以后:startInventory = 上期期末库存
// 公式:inventory[i][t-1] + x[i][t] + shortfall[i][t] = demand[i][t] + inventory[i][t]
// 移项后:inventory[i][t-1] + x[i][t] + shortfall[i][t] - inventory[i][t] = demand[i][t]
for (int i = 0; i < numProducts; i++) {
for (int t = 1; t < numDays; t++) {
MPConstraint inv = solver.makeConstraint(
demand[i][t], demand[i][t],
"inv_balance_" + products[i] + "_d" + (t + 1));
inv.setCoefficient(inventory[i][t - 1], 1); // 上期库存
for (int j = 0; j < numLines; j++) {
inv.setCoefficient(x[i][j][t], 1);//各产线当天产量
}
inv.setCoefficient(shortfall[i][t], 1); // 缺口补等式
inv.setCoefficient(inventory[i][t], -1); // 期末库存
}
}
// ---- 约束2:安全库存约束 ----
// 安全库存
double safetyStock = 10;
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
MPConstraint ss = solver.makeConstraint(safetyStock, Double.POSITIVE_INFINITY, "safety_stock_" + products[i] + "_d" + (t + 1));
ss.setCoefficient(inventory[i][t], 1);
// 期末库存=安全库存
}
}
// ---- 约束3:产能约束 ----
// ---- 生产效率 p[i][j]:产线j生产产品i的效率(件/小时)----
double[][] productivity = {
{20, 15}, // P1: L1=20件/时, L2=15件/时
{10, 8}, // P2: L1=10件/时, L2=8件/时
{15, 12} // P3: L1=15件/时, L2=12件/时
};
// ---- 每条产线每日产能(小时)----
double[] dailyCapacity = {10, 10}; // L1=10小时, L2=10小时
// ---- 换型时间 setupTime[i][j]:产线j切换到产品i所需时间(小时)----
double[][] setupTime = {
{0.5, 0.4}, // P1: L1换型0.5小时, L2换型0.4小时
{0.8, 0.6}, // P2: L1换型0.8小时, L2换型0.6小时
{0.6, 0.5} // P3: L1换型0.6小时, L2换型0.5小时
};
// ---- 约束3:产能约束(每条产线独立计算)----
//当天产量<=生产效率*每日产能
// 各产品产量/效率之和 + 换型时间消耗 ≤ 日产能
for (int j = 0; j < numLines; j++) {
for (int t = 0; t < numDays; t++) {
MPConstraint cap = solver.makeConstraint(0, dailyCapacity[j],
"capacity_" + lines[j] + "_d" + (t + 1));
for (int i = 0; i < numProducts; i++) {
cap.setCoefficient(x[i][j][t], 1.0 / productivity[i][j]);
cap.setCoefficient(switchTo[i][j][t], setupTime[i][j]);
}
}
}
// ---- 大M常数 ----
double bigM = 10000;
// ---- 约束4:生产开关约束(大M法,每条产线独立)----
// x[i][j][t] <= M * y[i][j][t]
for (int i = 0; i < numProducts; i++) {
for (int j = 0; j < numLines; j++) {
for (int t = 0; t < numDays; t++) {
MPConstraint sw = solver.makeConstraint(-Double.POSITIVE_INFINITY, 0,
"switch_" + products[i] + "_" + lines[j] + "_d" + (t + 1));
sw.setCoefficient(x[i][j][t], 1);
sw.setCoefficient(y[i][j][t], -bigM);
}
}
}
//约束5--影响y>约束4---影响y--影响x> x=0的话约束1不成立 初始库存(20) + 当天产量(0) = 当天需求(80) + 期末库存(10) 不成立无解
// ============================================================================
// 换型顺序相关约束(支持任意数量产品)
//
// 【核心思想】使用位置变量 s[i][k][j][t] 追踪产品在产线上的生产顺序
// - s[i][k][j][t] = 1: 产线j第t天的第k个位置生产产品i
// - k=0 表示第一个位置,k=1 表示第二个位置,以此类推
//
// 【数据实例】假设某天L1生产顺序为 P2 → P1 → P3:
// s[P2][0][L1][day] = 1 (P2在第一个位置)
// s[P1][1][L1][day] = 1 (P1在第二个位置)
// s[P3][2][L1][day] = 1 (P3在第三个位置)
// 其他 s[i][k][L1][day] = 0
//
// 【换型成本计算规则】
// 1. 第一个位置的产品 (k=0) 不收取换型成本(当天首次生产)
// 2. 位置 k>0 的产品收取换型成本(当天切换)
// 3. 如果前一天最后一个产品 ≠ 当天第一个产品,收取跨天换型成本
//
// 【数据实例】假设Day1 L1生产P2,Day2 L1生产P3 → P1:
// Day1: lastProduct[P2][L1][d1] = 1 (P2是最后产品)
// Day2: s[P3][0][L1][d2] = 1 (P3是第一个产品)
// 因为 P2 ≠ P3,所以 switchTo[P3][L1][d2] = 1 (跨天切换)
// 因为 P1在位置1>0,所以 switchTo[P1][L1][d2] = 1 (当天切换)
// ============================================================================
// ---- 约束5:位置变量s的约束 ----
// 每个位置k最多分配一个产品
// 【数据实例】位置k=0可以生产P1、P2或P3中的任意一个,但只能选一个
// s[P1][0] + s[P2][0] + s[P3][0] ≤ 1
for (int j = 0; j < numLines; j++) {
for (int t = 0; t < numDays; t++) {
for (int k = 0; k < maxProductsPerDay; k++) {
MPConstraint posOne = solver.makeConstraint(0, 1,
"pos_one_" + lines[j] + "_d" + (t + 1) + "_pos" + (k + 1));
for (int i = 0; i < numProducts; i++) {
posOne.setCoefficient(s[i][k][j][t], 1);
//0<=s_P1_pos1_L1_d1,s_P2_pos1_L1_d1,s_P3_pos1_L1_d1<=1,最好只能生产一个产品
//0<=s_P1_pos2_L1_d1,s_P2_pos2_L1_d1,s_P3_pos2_L1_d1<=1,
}
}
}
}
// 5.2 每个产品在当天最多占用一个位置(如果生产了该产品)
// 【数据实例】如果L1生产P2,那么P2只能在位置0、1、2中的一个:
// s[P2][0] + s[P2][1] + s[P2][2] ≤ 1
for (int i = 0; i < numProducts; i++) {
for (int j = 0; j < numLines; j++) {
for (int t = 0; t < numDays; t++) {
MPConstraint prodOne = solver.makeConstraint(0, 1,
"prod_one_" + products[i] + "_" + lines[j] + "_d" + (t + 1));
for (int k = 0; k < maxProductsPerDay; k++) {
prodOne.setCoefficient(s[i][k][j][t], 1);
//0<=s_P1_pos1_L1_d1,s_P1_pos2_L1_d1,s_P1_pos3_L1_d1<=1一个产品一个产线一天最多生产一次
}
}
}
}
// 5.3 如果产品i在当天被生产(y[i]=1),则必须分配到某个位置
// 【数据实例】如果 y[P2][L1][d1] = 1,那么:
// s[P2][0] + s[P2][1] + s[P2][2] = 1
// (P2必须在某个位置生产)
for (int i = 0; i < numProducts; i++) {
for (int j = 0; j < numLines; j++) {
for (int t = 0; t < numDays; t++) {
MPConstraint prodPos = solver.makeConstraint(0, 0,
"prod_pos_" + products[i] + "_" + lines[j] + "_d" + (t + 1));
for (int k = 0; k < maxProductsPerDay; k++) {
prodPos.setCoefficient(s[i][k][j][t], 1);
}
prodPos.setCoefficient(y[i][j][t], -1);
}
}
}
// 5.4 位置必须连续:如果位置k被占用,则位置0到k-1也必须被占用
// 【数据实例】如果生产顺序是 P2 → P1 → P3(3个产品):
// 位置0: s[P2][0]=1, s[P1][0]=0, s[P3][0]=0, sum=1
// 位置1: s[P2][1]=0, s[P1][1]=1, s[P3][1]=0, sum=1
// 位置2: s[P2][2]=0, s[P1][2]=0, s[P3][2]=1, sum=1
// 约束要求: sum(pos1) ≤ sum(pos0) → 1 ≤ 1 ✓
// sum(pos2) ≤ sum(pos1) → 1 ≤ 1 ✓
// 反例:如果位置0没有产品,但位置1有产品,这是不允许的
for (int j = 0; j < numLines; j++) {
for (int t = 0; t < numDays; t++) {
for (int k = 1; k < maxProductsPerDay; k++) {
// sum(s[i][k]) <= sum(s[i][k-1])
// 如果位置k有产品,则位置k-1必须有产品
MPConstraint cont = solver.makeConstraint(-Double.POSITIVE_INFINITY, 0,
"cont_" + lines[j] + "_d" + (t + 1) + "_pos" + (k + 1));
for (int i = 0; i < numProducts; i++) {
cont.setCoefficient(s[i][k][j][t], 1);
cont.setCoefficient(s[i][k - 1][j][t], -1);
// s_P1_pos2_L1_d1-s_P1_pos1_L1_d1=0;
}
}
}
}
// ---- 约束6:lastProduct变量的约束 ----
// lastProduct[i][j][t] = 1 当且仅当产品i是产线j第t天最后生产的产品
//
// 【数据实例】生产顺序 P2 → P1 → P3:
// lastProduct[P2] = 0 (P2后面还有P1和P3)
// lastProduct[P1] = 0 (P1后面还有P3)
// lastProduct[P3] = 1 (P3是最后一个)
//
// 【数据实例】生产顺序 P2 → P1(只生产2个产品):
// lastProduct[P2] = 0 (P2后面还有P1)
// lastProduct[P1] = 1 (P1是最后一个)
// lastProduct[P3] = 0 (P3根本没生产)
for (int i = 0; i < numProducts; i++) {
for (int j = 0; j < numLines; j++) {
for (int t = 0; t < numDays; t++) {
// 6.1 上界约束:lastProduct[i] ≤ sum(s[i][k])
// 只有生产了i才能成为lastProduct
// 【数据实例】如果P2没生产(sum(s[P2][k])=0),则lastProduct[P2] ≤ 0
MPConstraint lastUpper = solver.makeConstraint(-Double.POSITIVE_INFINITY, 0,
"last_upper_" + products[i] + "_" + lines[j] + "_d" + (t + 1));
lastUpper.setCoefficient(lastProduct[i][j][t], 1);
for (int k = 0; k < maxProductsPerDay; k++) {
lastUpper.setCoefficient(s[i][k][j][t], -1);
}
//s_P1_pos1_L1_d1,s_P1_pos2_L1_d1,s_P1_pos3_L1_d1-lastProduct[P1][L1][d1]<=0
// 6.2 下界约束:lastProduct[i] ≥ s[i][k] - sum(s[all][k+1])
// 如果产品i在位置k,且位置k+1没有产品,则lastProduct[i]必须为1
// 【数据实例】生产顺序 P2 → P1 → P3:
// 对于P3在位置2: lastProduct[P3] ≥ s[P3][2] - 0 (k=2是最后位置)
// 对于P1在位置1: lastProduct[P1] ≥ s[P1][1] - sum(s[all][2]) = 1 - 1 = 0
// 对于P2在位置0: lastProduct[P2] ≥ s[P2][0] - sum(s[all][1]) = 1 - 1 = 0
for (int k = 0; k < maxProductsPerDay; k++) {
MPConstraint lastLower = solver.makeConstraint(0, Double.POSITIVE_INFINITY,
"last_lower_" + products[i] + "_" + lines[j] + "_d" + (t + 1) + "_pos" + k);
lastLower.setCoefficient(lastProduct[i][j][t], 1);
lastLower.setCoefficient(s[i][k][j][t], -1);
if (k < maxProductsPerDay - 1) {
for (int ii = 0; ii < numProducts; ii++) {
lastLower.setCoefficient(s[ii][k + 1][j][t], 1);
}
}
//lastProduct[P1][L1][d2]=P1_pos1_L1_d2-
// (P1_pos2_L1_d2+P1_pos3_L1_d2+
// P2_pos2_L1_d2+P2_pos3_L1_d2+
// P3_pos2_L1_d2+P3_pos3_L1_d2)
}
// 6.3 关键约束:如果产品i在位置k生产,且位置k+1有任何产品生产,则lastProduct[i]必须为0
// lastProduct[i] ≤ 2 - s[i][k] - sum(s[all][k+1])
// 当s[i][k]=1且sum(s[all][k+1])=1时:lastProduct[i] ≤ 0
// 当s[i][k]=1且sum(s[all][k+1])=0时:lastProduct[i] ≤ 1(允许是最后产品)
// 【数据实例1】只生产P2(s[P2][0]=1, sum(s[all][1])=0):
// lastProduct[P2] ≤ 2 - 1 - 0 = 1 ✓(P2可以是最后产品)
// 【数据实例2】生产顺序 P2 → P1 → P3:
// 对于P2在位置0: lastProduct[P2] ≤ 2 - 1 - sum(s[all][1]) = 2 - 1 - 1 = 0 ✓
// 对于P1在位置1: lastProduct[P1] ≤ 2 - 1 - sum(s[all][2]) = 2 - 1 - 1 = 0 ✓
for (int k = 0; k < maxProductsPerDay - 1; k++) {
MPConstraint lastBeforeNext = solver.makeConstraint(-Double.POSITIVE_INFINITY, 2,
"last_before_next_" + products[i] + "_" + lines[j] + "_d" + (t + 1) + "_pos" + k);
lastBeforeNext.setCoefficient(lastProduct[i][j][t], 1);
lastBeforeNext.setCoefficient(s[i][k][j][t], -1);
for (int ii = 0; ii < numProducts; ii++) {
lastBeforeNext.setCoefficient(s[ii][k + 1][j][t], -1);
}
}
}
}
}
// switchTo[i][j][t] = 1 表示需要切换到产品i(产生换型成本)
//
// 换型触发条件(满足任一即触发):
// 7.1 同一天切换:产品i在位置k > 0(不是第一个生产的)
// 7.2 跨天切换:产品i在位置k=0(第一个),且前一天最后一个产品 ≠ i
//
// 【数据实例1 - 同一天切换】生产顺序 P2 → P1 → P3:
// switchTo[P2] = 0 (P2是第一个,不需要切换)
// switchTo[P1] = 1 (P1在位置1>0,需要切换)
// switchTo[P3] = 1 (P3在位置2>0,需要切换)
//
// 【数据实例2 - 跨天切换】Day1生产P2,Day2生产P3 → P1:
// Day1: lastProduct[P2] = 1
// Day2: switchTo[P3] = 1 (P3是第一个产品,但前一天最后产品是P2≠P3)
// Day2: switchTo[P1] = 1 (P1在位置1>0)
// Day2: switchTo[P2] = 0 (P2没生产)
for (int i = 0; i < numProducts; i++) {
for (int j = 0; j < numLines; j++) {
for (int t = 0; t < numDays; t++) {
// 7.1 同一天切换到i:如果i在位置k > 0,则需要切换
// switchTo[i] >= s[i][k] 对于 k > 0
// 【数据实例】P1在位置1: switchTo[P1] ≥ s[P1][1] = 1
for (int k = 1; k < maxProductsPerDay; k++) {
MPConstraint stSame = solver.makeConstraint(0, Double.POSITIVE_INFINITY,
"switchTo_same_" + products[i] + "_" + lines[j] + "_d" + (t + 1) + "_pos" + k);
stSame.setCoefficient(switchTo[i][j][t], 1);
stSame.setCoefficient(s[i][k][j][t], -1);
}
// 7.2 跨天切换到i(第2天及以后)
if (t >= 1) {
// 如果i在当天的第一个位置(k=0),且昨天最后生产的产品 ≠ i
// 使用lastProduct[other][j][t-1]精确识别昨天最后生产的产品
// 【数据实例】Day1 lastProduct[P2]=1,Day2 s[P3][0]=1:
// 因为P2≠P3,所以switchTo[P3] ≥ lastProduct[P2] + s[P3][0] - 1 = 1 + 1 - 1 = 1
for (int other = 0; other < numProducts; other++) {
if (other == i) continue;
// 如果other是昨天的最后一个产品,且i是今天的第一个产品,则需要切换
MPConstraint stCross = solver.makeConstraint(0, Double.POSITIVE_INFINITY,
"switchTo_cross_" + products[i] + "_from_" + products[other] + "_" + lines[j] + "_d" + (t + 1));
stCross.setCoefficient(switchTo[i][j][t], 1);
stCross.setCoefficient(lastProduct[other][j][t - 1], -1);
stCross.setCoefficient(s[i][0][j][t], -1);
}
}
// 7.3 上界约束:switchTo[i] ≤ y[i]
// 只有生产了i才能切换到i(不生产就不需要切换)
// 【数据实例】如果y[P2]=0,则switchTo[P2] ≤ 0
MPConstraint stUpper = solver.makeConstraint(-Double.POSITIVE_INFINITY, 0,
"switchTo_upper_" + products[i] + "_" + lines[j] + "_d" + (t + 1));
stUpper.setCoefficient(switchTo[i][j][t], 1);
stUpper.setCoefficient(y[i][j][t], -1);
}
}
}
// ========== 7. 求解 ==========
System.out.println("========== 开始求解(多产线MIP排产)==========");
System.out.println("产线数量:" + numLines + ",产品数量:" + numProducts + ",天数:" + numDays);
System.out.println();
MPSolver.ResultStatus status = solver.solve();
// ========== 8. 输出结果 ==========
if (status == MPSolver.ResultStatus.OPTIMAL) {
System.out.println("✅ 找到最优解!");
System.out.printf("最小总成本:%.2f 元%n%n", objective.value());
// 按天输出
for (int t = 0; t < numDays; t++) {
System.out.println("━━━━━━━━━━━━ 第 " + (t + 1) + " 天 ━━━━━━━━━━━━");
// 每条产线的排产
for (int j = 0; j < numLines; j++) {
System.out.println(" 【" + lines[j] + "】");
double lineHours = 0;
double lineSetupHours = 0;
boolean produced = false;
// 确定生产顺序
String[] sequence = new String[maxProductsPerDay];
int seqLength = 0;
for (int k = 0; k < maxProductsPerDay; k++) {
for (int i = 0; i < numProducts; i++) {
if (s[i][k][j][t].solutionValue() > 0.001) {
sequence[seqLength++] = products[i];
break;
}
}
}
// 输出生产顺序
if (seqLength > 0) {
System.out.print(" 生产顺序:");
for (int k = 0; k < seqLength; k++) {
if (k > 0) System.out.print(" → ");
System.out.print(sequence[k]);
}
System.out.println();
// 输出当天切换信息
for (int k = 0; k < seqLength - 1; k++) {
int fromIdx = -1, toIdx = -1;
for (int idx = 0; idx < numProducts; idx++) {
if (products[idx].equals(sequence[k])) fromIdx = idx;
if (products[idx].equals(sequence[k + 1])) toIdx = idx;
}
if (fromIdx >= 0 && toIdx >= 0) {
System.out.printf(" → 当天切换:%s → %s(换型成本+%.0f元,换型时间+%.1fh)%n",
sequence[k], sequence[k + 1],
setupCost[toIdx][j], setupTime[toIdx][j]);
}
}
}
// 输出跨天切换信息(第2天及以后)
if (t >= 1 && seqLength > 0) {
// 找到昨天的最后一个产品
String prevLastProd = "无";
for (int i = 0; i < numProducts; i++) {
if (lastProduct[i][j][t - 1].solutionValue() > 0.001) {
prevLastProd = products[i];
break;
}
}
if (!prevLastProd.equals("无") && !prevLastProd.equals(sequence[0])) {
int toIdx = -1;
for (int idx = 0; idx < numProducts; idx++) {
if (products[idx].equals(sequence[0])) toIdx = idx;
}
System.out.printf(" → 跨天切换:%s → %s(换型成本+%.0f元,换型时间+%.1fh)%n",
prevLastProd, sequence[0],
setupCost[toIdx][j], setupTime[toIdx][j]);
}
}
for (int i = 0; i < numProducts; i++) {
double qty = x[i][j][t].solutionValue();
double hours = qty / productivity[i][j];
double setupHr = switchTo[i][j][t].solutionValue() * setupTime[i][j];
lineHours += hours;
lineSetupHours += setupHr;
if (qty > 0.001) {
produced = true;
double cost = qty * unitCost[i][j];
double setupCostVal = switchTo[i][j][t].solutionValue() * setupCost[i][j];
System.out.printf(" → 生产 %s:%.0f 件,耗时 %.1fh,成本 %.0f 元",
products[i], qty, hours, cost);
if (setupCostVal > 0) {
System.out.printf("(换型成本+%.0f,换型时间+%.1fh)", setupCostVal, setupHr);
}
System.out.println();
}
}
if (!produced) {
System.out.println(" (闲置)");
}
System.out.printf(" 工时利用:生产%.1fh + 换型%.1fh = %.1f / %.1f 小时 (%.0f%%)%n",
lineHours, lineSetupHours, lineHours + lineSetupHours,
dailyCapacity[j], (lineHours + lineSetupHours) / dailyCapacity[j] * 100);
System.out.println();
}
// 库存
System.out.println(" 【期末库存】");
for (int i = 0; i < numProducts; i++) {
System.out.printf(" %s:%.0f 件%n", products[i], inventory[i][t].solutionValue());
}
System.out.println();
}
// 成本明细
double totalProdCost = 0, totalHoldCost = 0, totalSetupCost = 0, totalSetupTime = 0;
for (int i = 0; i < numProducts; i++) {
for (int j = 0; j < numLines; j++) {
for (int t = 0; t < numDays; t++) {
totalProdCost += x[i][j][t].solutionValue() * unitCost[i][j];
totalSetupCost += switchTo[i][j][t].solutionValue() * setupCost[i][j];
totalSetupTime += switchTo[i][j][t].solutionValue() * setupTime[i][j];
}
}
}
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
totalHoldCost += inventory[i][t].solutionValue() * holdingCost[i];
}
}
System.out.println("================ 成本明细 ================");
System.out.printf("生产成本: %8.2f 元%n", totalProdCost);
System.out.printf("库存成本: %8.2f 元%n", totalHoldCost);
System.out.printf("换型成本: %8.2f 元%n", totalSetupCost);
System.out.printf("────────────────────────────%n");
System.out.printf("总成本: %8.2f 元%n", objective.value());
// 换型时间统计
System.out.println();
System.out.println("============ 换型时间统计 ============");
System.out.printf(" 换型总耗时:%.1f 小时%n", totalSetupTime);
// 产线利用率统计(包含换型时间)
System.out.println();
System.out.println("============ 产线利用率统计 ============");
for (int j = 0; j < numLines; j++) {
double totalProdHours = 0, totalSetupHours = 0;
for (int t = 0; t < numDays; t++) {
for (int i = 0; i < numProducts; i++) {
totalProdHours += x[i][j][t].solutionValue() / productivity[i][j];
totalSetupHours += switchTo[i][j][t].solutionValue() * setupTime[i][j];
}
}
double totalHours = totalProdHours + totalSetupHours;
double totalCap = dailyCapacity[j] * numDays;
System.out.printf(" %s:生产%.1fh + 换型%.1fh = %.1f / %.1f 小时 (利用率 %.1f%%)%n",
lines[j], totalProdHours, totalSetupHours, totalHours, totalCap, totalHours / totalCap * 100);
}
} else if (status == MPSolver.ResultStatus.INFEASIBLE) {
System.out.println("❌ 模型无解(INFEASIBLE)——产能不足,无法满足所有需求");
} else {
System.out.println("求解状态:" + status);
}
MPModelExportOptions options=new MPModelExportOptions();
String lpText = solver.exportModelAsLpFormat(false);
// FileHelper.writeFile(lpText,"model.lp");
String mpsText = solver.exportModelAsMpsFormat(true,true);
// FileHelper.writeFile(mpsText,"model.mps");
// 输出LP模型到控制台
System.out.println("========== LP模型输出 ==========");
// System.out.println(lpText);
for(MPVariable var : solver.variables()){
String name = var.name();
double val = var.solutionValue();
double lb = var.lb();
double ub = var.ub();
System.out.printf("变量[%s] 下界=%.2f 上界=%.2f 最优解=%.4f%n",
name, lb, ub, val);
}
// 求解统计
System.out.println();
System.out.println("============ 求解统计 ============");
System.out.println("变量总数:" + solver.numVariables());
System.out.println("约束总数:" + solver.numConstraints());
System.out.printf("求解时间:%.3f 秒%n", solver.wallTime() / 1000.0);
}
}
package com.aps.service.mp;
import com.google.ortools.Loader;
import com.google.ortools.linearsolver.MPConstraint;
import com.google.ortools.linearsolver.MPObjective;
import com.google.ortools.linearsolver.MPSolver;
import com.google.ortools.linearsolver.MPVariable;
import java.util.ArrayList;
import java.util.List;
/**
* 多工序多设备 日级生产计划 MIP 模型
* 核心思路:用 WIP 在制品库存衔接工序先后,每台设备有日产能约束
*
* P1: 工序1(M1) → WIP1 → 工序2(M2) → WIP2 → 工序3(M3) → 成品
* P2: 工序1(M2) → WIP1 → 工序2(M3) → 成品
*
* 目标:最小化 生产成本 + 换型成本 + 库存持有成本
*/
public class MultiOperationDailyPlanning {
// 工序定义
static class Operation {
String product; // 产品名
int opIndex; // 工序序号(从0开始)
int machine; // 设备编号
double rate; // 加工效率(件/小时)
double unitCost; // 单位加工成本(元/件)
double setupCost; // 换型成本(元/次)
double wipCost; // 该工序后WIP的持有成本(元/件/天)
Operation(String product, int opIndex, int machine,
double rate, double unitCost, double setupCost, double wipCost) {
this.product = product;
this.opIndex = opIndex;
this.machine = machine;
this.rate = rate;
this.unitCost = unitCost;
this.setupCost = setupCost;
this.wipCost = wipCost;
}
}
public static void main(String[] args) {
Loader.loadNativeLibraries();
// ========== 1. 基础参数 ==========
String[] machineNames = {"M1", "M2", "M3"};
int numMachines = machineNames.length;
double[] dailyCapacity = {10, 12, 8}; // 各设备日产能(小时)
String[] products = {"P1", "P2"};
int numProducts = products.length;
int numDays = 4; // 计划周期:4天
// 成品需求
double[][] demand = {
{50, 60, 40, 70}, // P1 日需求
{30, 40, 50, 35} // P2 日需求
};
// 成品初始库存、持有成本
double[] initFgInv = {20, 10};
double[] fgHoldCost = {1.0, 1.5};
// ========== 2. 工艺路线定义 ==========
// 每个产品的工序列表(按先后顺序)
List<List<Operation>> routes = new ArrayList<>();
// P1: 3道工序,M1 → M2 → M3
List<Operation> p1Route = new ArrayList<>();
p1Route.add(new Operation("P1", 0, 0, 25, 2.0, 100, 0.3)); // 工序1: M1, 25件/h
p1Route.add(new Operation("P1", 1, 1, 20, 3.0, 150, 0.4)); // 工序2: M2, 20件/h
p1Route.add(new Operation("P1", 2, 2, 30, 2.5, 80, 0.0)); // 工序3: M3, 30件/h (末道无WIP)
routes.add(p1Route);
// P2: 2道工序,M2 → M3
List<Operation> p2Route = new ArrayList<>();
p2Route.add(new Operation("P2", 0, 1, 15, 4.0, 120, 0.5)); // 工序1: M2, 15件/h
p2Route.add(new Operation("P2", 1, 2, 20, 3.5, 100, 0.0)); // 工序2: M3, 20件/h (末道无WIP)
routes.add(p2Route);
// WIP初始库存(每道工序后)
double[][] initWipInv = {
{30, 20, 0}, // P1: 工序1后30件, 工序2后20件
{25, 0} // P2: 工序1后25件
};
double bigM = 10000;
// ========== 3. 创建求解器 ==========
MPSolver solver = MPSolver.createSolver("CBC");
// ========== 4. 决策变量 ==========
// x[i][k][t]: 第t天产品i的第k道工序产量
MPVariable[][][] x = new MPVariable[numProducts][][];
// y[i][k][t]: 第t天产品i的第k道工序是否生产(0-1,换型用)
MPVariable[][][] y = new MPVariable[numProducts][][];
// wip[i][k][t]: 第t天末产品i第k道工序后的在制品库存
MPVariable[][][] wip = new MPVariable[numProducts][][];
for (int i = 0; i < numProducts; i++) {
int numOps = routes.get(i).size();
x[i] = new MPVariable[numOps][numDays];
y[i] = new MPVariable[numOps][numDays];
wip[i] = new MPVariable[numOps][numDays];
for (int k = 0; k < numOps; k++) {
Operation op = routes.get(i).get(k);
for (int t = 0; t < numDays; t++) {
x[i][k][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
"x_" + op.product + "_op" + (k + 1) + "_d" + (t + 1));
y[i][k][t] = solver.makeBoolVar(
"y_" + op.product + "_op" + (k + 1) + "_d" + (t + 1));
// 末道工序没有WIP(直接进成品库),但变量还是创建,值为0即可
wip[i][k][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
"wip_" + op.product + "_op" + (k + 1) + "_d" + (t + 1));
}
}
}
// 成品库存 fg[i][t]
MPVariable[][] fg = new MPVariable[numProducts][numDays];
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
fg[i][t] = solver.makeNumVar(0, Double.POSITIVE_INFINITY,
"fg_" + products[i] + "_d" + (t + 1));
}
}
// ========== 5. 目标函数 ==========
MPObjective obj = solver.objective();
// 生产成本 + 换型成本
for (int i = 0; i < numProducts; i++) {
for (int k = 0; k < routes.get(i).size(); k++) {
Operation op = routes.get(i).get(k);
for (int t = 0; t < numDays; t++) {
obj.setCoefficient(x[i][k][t], op.unitCost);
obj.setCoefficient(y[i][k][t], op.setupCost);
}
}
}
// WIP持有成本(非末道工序)
for (int i = 0; i < numProducts; i++) {
int numOps = routes.get(i).size();
for (int k = 0; k < numOps - 1; k++) { // 末道没有WIP
Operation op = routes.get(i).get(k);
for (int t = 0; t < numDays; t++) {
obj.setCoefficient(wip[i][k][t], op.wipCost);
}
}
}
// 成品持有成本
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
obj.setCoefficient(fg[i][t], fgHoldCost[i]);
}
}
obj.setMinimization();
// ========== 6. 约束条件 ==========
// ===== 约束1:成品库存平衡 =====
// 期初成品 + 末道工序产量 = 需求 + 期末成品
for (int i = 0; i < numProducts; i++) {
int lastOp = routes.get(i).size() - 1;
for (int t = 0; t < numDays; t++) {
double prevInv = (t == 0) ? initFgInv[i] : 0;
MPConstraint c = solver.makeConstraint(
demand[i][t] - prevInv, demand[i][t] - prevInv,
"fg_inv_" + products[i] + "_d" + (t + 1));
if (t > 0) c.setCoefficient(fg[i][t - 1], 1);
c.setCoefficient(x[i][lastOp][t], 1); // 末道工序产出 = 成品入库
c.setCoefficient(fg[i][t], -1);
}
}
// ===== 约束2:WIP在制品库存平衡(核心!工序先后约束)=====
// 期初WIP + 本工序产量 = 下道工序投入 + 期末WIP
// (同一天内本工序产出可直接供下工序使用,即"天内流水")
for (int i = 0; i < numProducts; i++) {
int numOps = routes.get(i).size();
for (int k = 0; k < numOps - 1; k++) { // 每道非末道工序都有WIP
for (int t = 0; t < numDays; t++) {
double prevWip = (t == 0) ? initWipInv[i][k] : 0;
MPConstraint c = solver.makeConstraint(-prevWip, -prevWip,
"wip_inv_" + products[i] + "_op" + (k + 1) + "_d" + (t + 1));
if (t > 0) c.setCoefficient(wip[i][k][t - 1], 1); // 期初WIP
c.setCoefficient(x[i][k][t], 1); // 本工序产出(+)
c.setCoefficient(x[i][k + 1][t], -1); // 下工序投入(-)
c.setCoefficient(wip[i][k][t], -1); // 期末WIP(-)
}
}
}
// ===== 约束3:设备产能约束 =====
// 每台设备每天所有工序的总工时 ≤ 日产能
for (int m = 0; m < numMachines; m++) {
for (int t = 0; t < numDays; t++) {
MPConstraint cap = solver.makeConstraint(0, dailyCapacity[m],
"cap_" + machineNames[m] + "_d" + (t + 1));
for (int i = 0; i < numProducts; i++) {
for (int k = 0; k < routes.get(i).size(); k++) {
Operation op = routes.get(i).get(k);
if (op.machine == m) {
cap.setCoefficient(x[i][k][t], 1.0 / op.rate);
}
}
}
}
}
// ===== 约束4:生产开关约束(换型用)=====
// x[i][k][t] <= M * y[i][k][t]
for (int i = 0; i < numProducts; i++) {
for (int k = 0; k < routes.get(i).size(); k++) {
Operation op = routes.get(i).get(k);
for (int t = 0; t < numDays; t++) {
MPConstraint c = solver.makeConstraint(-Double.POSITIVE_INFINITY, 0,
"switch_" + op.product + "_op" + (k + 1) + "_d" + (t + 1));
c.setCoefficient(x[i][k][t], 1);
c.setCoefficient(y[i][k][t], -bigM);
}
}
}
// ===== 约束5:末道工序WIP强制为0(直接入成品库)=====
for (int i = 0; i < numProducts; i++) {
int lastOp = routes.get(i).size() - 1;
for (int t = 0; t < numDays; t++) {
MPConstraint c = solver.makeConstraint(0, 0,
"wip_last_" + products[i] + "_d" + (t + 1));
c.setCoefficient(wip[i][lastOp][t], 1);
}
}
// ========== 7. 求解 ==========
System.out.println("========== 多工序多设备 日级排产 MIP ==========");
System.out.printf("产品:%d种,设备:%d台,周期:%d天%n",
numProducts, numMachines, numDays);
System.out.print("工艺路线:");
for (int i = 0; i < numProducts; i++) {
System.out.print(products[i] + "(");
for (int k = 0; k < routes.get(i).size(); k++) {
if (k > 0) System.out.print("→");
System.out.print(machineNames[routes.get(i).get(k).machine]);
}
System.out.print(") ");
}
System.out.println("\n");
MPSolver.ResultStatus status = solver.solve();
// ========== 8. 结果输出 ==========
if (status == MPSolver.ResultStatus.OPTIMAL) {
System.out.println("✅ 求解成功!全局最优解");
System.out.printf("最小总成本:%.2f 元%n%n", obj.value());
// ---- 按天输出 ----
for (int t = 0; t < numDays; t++) {
System.out.println("══════════════════ 第 " + (t + 1) + " 天 ══════════════════");
// 每台设备的排产
for (int m = 0; m < numMachines; m++) {
System.out.println(" 【" + machineNames[m] + "】 日产能 " + dailyCapacity[m] + "h");
double totalHours = 0;
boolean hasProd = false;
for (int i = 0; i < numProducts; i++) {
for (int k = 0; k < routes.get(i).size(); k++) {
Operation op = routes.get(i).get(k);
if (op.machine != m) continue;
double qty = x[i][k][t].solutionValue();
double hours = qty / op.rate;
totalHours += hours;
if (qty > 0.001) {
hasProd = true;
double setup = y[i][k][t].solutionValue() * op.setupCost;
System.out.printf(" %s-工序%d:生产 %6.1f 件,耗时 %5.1fh " +
"(加工费 %.0f元,换型 %.0f元)%n",
op.product, k + 1, qty, hours,
qty * op.unitCost, setup);
}
}
}
if (!hasProd) {
System.out.println(" (闲置)");
}
System.out.printf(" 工时合计:%.1f / %.1f h (利用率 %.0f%%)%n",
totalHours, dailyCapacity[m], totalHours / dailyCapacity[m] * 100);
System.out.println();
}
// 库存状态
System.out.println(" 【库存状态】");
for (int i = 0; i < numProducts; i++) {
System.out.print(" " + products[i] + ":");
// WIP
for (int k = 0; k < routes.get(i).size() - 1; k++) {
System.out.printf("WIP%d=%.0f ", k + 1, wip[i][k][t].solutionValue());
}
System.out.printf("成品=%.0f", fg[i][t].solutionValue());
System.out.println();
}
System.out.println();
}
// ---- 成本明细 ----
double totalProdCost = 0, totalSetupCost = 0, totalWipCost = 0, totalFgCost = 0;
for (int i = 0; i < numProducts; i++) {
for (int k = 0; k < routes.get(i).size(); k++) {
Operation op = routes.get(i).get(k);
for (int t = 0; t < numDays; t++) {
totalProdCost += x[i][k][t].solutionValue() * op.unitCost;
totalSetupCost += y[i][k][t].solutionValue() * op.setupCost;
if (k < routes.get(i).size() - 1) {
totalWipCost += wip[i][k][t].solutionValue() * op.wipCost;
}
}
}
}
for (int i = 0; i < numProducts; i++) {
for (int t = 0; t < numDays; t++) {
totalFgCost += fg[i][t].solutionValue() * fgHoldCost[i];
}
}
System.out.println("══════════════════ 成本明细 ══════════════════");
System.out.printf("加工成本: %8.2f 元%n", totalProdCost);
System.out.printf("换型成本: %8.2f 元%n", totalSetupCost);
System.out.printf("WIP 成本: %8.2f 元%n", totalWipCost);
System.out.printf("成品成本: %8.2f 元%n", totalFgCost);
System.out.printf("────────────────────────────%n");
System.out.printf("总成本: %8.2f 元%n", obj.value());
// 设备利用率总览
System.out.println();
System.out.println("══════════════════ 设备利用率总览 ══════════════════");
for (int m = 0; m < numMachines; m++) {
double totalHours = 0;
for (int i = 0; i < numProducts; i++) {
for (int k = 0; k < routes.get(i).size(); k++) {
Operation op = routes.get(i).get(k);
if (op.machine == m) {
for (int t = 0; t < numDays; t++) {
totalHours += x[i][k][t].solutionValue() / op.rate;
}
}
}
}
double totalCap = dailyCapacity[m] * numDays;
System.out.printf(" %s:总工时 %6.1f / %6.1f h (利用率 %.1f%%)%n",
machineNames[m], totalHours, totalCap, totalHours / totalCap * 100);
}
System.out.println();
System.out.println("══════════════════ 求解统计 ══════════════════");
System.out.println("变量数:" + solver.numVariables());
System.out.println("约束数:" + solver.numConstraints());
System.out.printf("求解时间:%.3f 秒%n", solver.wallTime() / 1000.0);
} else if (status == MPSolver.ResultStatus.INFEASIBLE) {
System.out.println("❌ 无解 —— 产能不足,无法满足所有需求");
// 可以输出哪个设备是瓶颈
} else {
System.out.println("求解状态:" + status);
}
}
}
package com.aps.service.mp;
/**
* 作者:佟礼
* 时间:2026-07-29
* 产品层配置:封装某一层产品的所有参数
*/
public class ProductLayerConfig {
String[] names; // 产品名称数组
double[] prodCost; // 单位生产成本
double[] prodRate; // 生产效率(件/小时)
double[] setupCost; // 换型成本
double[] holdCost; // 库存持有成本
double[] initInv; // 初始库存
double dailyCapacity; // 产线日产能(小时)
ProductLayerConfig(String[] names, double[] prodCost, double[] prodRate,
double[] setupCost, double[] holdCost, double[] initInv,
double dailyCapacity) {
this.names = names;
this.prodCost = prodCost;
this.prodRate = prodRate;
this.setupCost = setupCost;
this.holdCost = holdCost;
this.initInv = initInv;
this.dailyCapacity = dailyCapacity;
}
int size() { return names.length; }
}
package com.aps.service.mp;
import com.google.ortools.Loader;
import com.google.ortools.linearsolver.MPConstraint;
import com.google.ortools.linearsolver.MPObjective;
import com.google.ortools.linearsolver.MPSolver;
import com.google.ortools.linearsolver.MPVariable;
/**
* 作者:佟礼
* 时间:2026-07-29
* 产品层变量:封装某一层的所有决策变量
*/
public class ProductLayerVariables {
MPVariable[][] production; // 产量变量 [item][day]
MPVariable[][] inventory; // 库存变量 [item][day]
MPVariable[][] switchVar; // 生产开关变量 [item][day](半成品/成品生产用)
MPVariable[][] purchase; // 采购变量 [item][day](仅原材料层用)
}
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