Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
H
HYH.APSJ
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
佟礼
HYH.APSJ
Commits
48800682
Commit
48800682
authored
Aug 06, 2026
by
Tong Li
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
MP
parent
74589725
Hide whitespace changes
Inline
Side-by-side
Showing
24 changed files
with
7855 additions
and
1 deletion
+7855
-1
pom.xml
pom.xml
+1
-1
FileHelper.java
src/main/java/com/aps/common/util/FileHelper.java
+19
-0
TeePrintStream.java
src/main/java/com/aps/common/util/TeePrintStream.java
+41
-0
MacroPlannerOptimizer.java
...main/java/com/aps/macroplanner/MacroPlannerOptimizer.java
+456
-0
RoutingTestRunner.java
src/main/java/com/aps/macroplanner/RoutingTestRunner.java
+33
-0
DemandSlackLinkageConstraint.java
...macroplanner/constraint/DemandSlackLinkageConstraint.java
+91
-0
DataValidator.java
src/main/java/com/aps/macroplanner/data/DataValidator.java
+410
-0
InTransitSupply.java
src/main/java/com/aps/macroplanner/data/InTransitSupply.java
+39
-0
InitialInventory.java
...main/java/com/aps/macroplanner/data/InitialInventory.java
+27
-0
OperationOutput.java
src/main/java/com/aps/macroplanner/data/OperationOutput.java
+38
-0
ProductSpMapping.java
...main/java/com/aps/macroplanner/data/ProductSpMapping.java
+27
-0
RoutingTestDataBuilder.java
...ava/com/aps/macroplanner/data/RoutingTestDataBuilder.java
+124
-0
StrategyLevel.java
...in/java/com/aps/macroplanner/objective/StrategyLevel.java
+136
-0
BomMpsScheduling.java
src/main/java/com/aps/service/mp/BomMpsScheduling.java
+687
-0
BomMpsScheduling1.java
src/main/java/com/aps/service/mp/BomMpsScheduling1.java
+1314
-0
BomMpsScheduling2.java
src/main/java/com/aps/service/mp/BomMpsScheduling2.java
+2223
-0
InitVariablesProduct.java
src/main/java/com/aps/service/mp/InitVariablesProduct.java
+59
-0
MpsSchedulingDemo.java
src/main/java/com/aps/service/mp/MpsSchedulingDemo.java
+289
-0
MpsSchedulingDemo2.java
src/main/java/com/aps/service/mp/MpsSchedulingDemo2.java
+368
-0
MpsSchedulingDemo22.java
src/main/java/com/aps/service/mp/MpsSchedulingDemo22.java
+288
-0
MultiLineMpsScheduling.java
src/main/java/com/aps/service/mp/MultiLineMpsScheduling.java
+754
-0
MultiOperationDailyPlanning.java
.../java/com/aps/service/mp/MultiOperationDailyPlanning.java
+377
-0
ProductLayerConfig.java
src/main/java/com/aps/service/mp/ProductLayerConfig.java
+31
-0
ProductLayerVariables.java
src/main/java/com/aps/service/mp/ProductLayerVariables.java
+23
-0
No files found.
pom.xml
View file @
48800682
...
...
@@ -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) -->
...
...
src/main/java/com/aps/common/util/FileHelper.java
View file @
48800682
...
...
@@ -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
src/main/java/com/aps/common/util/TeePrintStream.java
0 → 100644
View file @
48800682
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
();
}
}
src/main/java/com/aps/macroplanner/MacroPlannerOptimizer.java
0 → 100644
View file @
48800682
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
src/main/java/com/aps/macroplanner/RoutingTestRunner.java
0 → 100644
View file @
48800682
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
src/main/java/com/aps/macroplanner/constraint/DemandSlackLinkageConstraint.java
0 → 100644
View file @
48800682
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
src/main/java/com/aps/macroplanner/data/DataValidator.java
0 → 100644
View file @
48800682
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
src/main/java/com/aps/macroplanner/data/InTransitSupply.java
0 → 100644
View file @
48800682
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
src/main/java/com/aps/macroplanner/data/InitialInventory.java
0 → 100644
View file @
48800682
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
src/main/java/com/aps/macroplanner/data/OperationOutput.java
0 → 100644
View file @
48800682
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
src/main/java/com/aps/macroplanner/data/ProductSpMapping.java
0 → 100644
View file @
48800682
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
src/main/java/com/aps/macroplanner/data/RoutingTestDataBuilder.java
0 → 100644
View file @
48800682
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
src/main/java/com/aps/macroplanner/objective/StrategyLevel.java
0 → 100644
View file @
48800682
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
src/main/java/com/aps/service/mp/BomMpsScheduling.java
0 → 100644
View file @
48800682
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
src/main/java/com/aps/service/mp/BomMpsScheduling1.java
0 → 100644
View file @
48800682
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
src/main/java/com/aps/service/mp/BomMpsScheduling2.java
0 → 100644
View file @
48800682
This source diff could not be displayed because it is too large. You can
view the blob
instead.
src/main/java/com/aps/service/mp/InitVariablesProduct.java
0 → 100644
View file @
48800682
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
;
}
}
src/main/java/com/aps/service/mp/MpsSchedulingDemo.java
0 → 100644
View file @
48800682
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
);
}
}
src/main/java/com/aps/service/mp/MpsSchedulingDemo2.java
0 → 100644
View file @
48800682
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
);
}
}
src/main/java/com/aps/service/mp/MpsSchedulingDemo22.java
0 → 100644
View file @
48800682
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
);
}
}
src/main/java/com/aps/service/mp/MultiLineMpsScheduling.java
0 → 100644
View file @
48800682
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
);
}
}
src/main/java/com/aps/service/mp/MultiOperationDailyPlanning.java
0 → 100644
View file @
48800682
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
);
}
}
}
src/main/java/com/aps/service/mp/ProductLayerConfig.java
0 → 100644
View file @
48800682
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
;
}
}
src/main/java/com/aps/service/mp/ProductLayerVariables.java
0 → 100644
View file @
48800682
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](仅原材料层用)
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment