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
bf94442c
Commit
bf94442c
authored
Aug 31, 2026
by
Tong Li
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
POA
parent
24eee50e
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
1297 additions
and
369 deletions
+1297
-369
optimization_result_1.json
mp/result/optimization_result_1.json
+1
-1
POAMacroPlanTest.java
src/main/java/com/aps/demo/POAMacroPlanTest.java
+275
-21
Entry.java
src/main/java/com/aps/entity/basic/Entry.java
+26
-4
MaterialInventoryConstraint.java
...a/com/aps/poa/constraint/MaterialInventoryConstraint.java
+62
-39
BatchSplitter.java
src/main/java/com/aps/poa/data/BatchSplitter.java
+242
-0
DataAdapter.java
src/main/java/com/aps/poa/data/DataAdapter.java
+20
-0
InventoryPolicy.java
src/main/java/com/aps/poa/data/InventoryPolicy.java
+20
-3
ModelConfig.java
src/main/java/com/aps/poa/data/ModelConfig.java
+2
-0
MrpExploder.java
src/main/java/com/aps/poa/data/MrpExploder.java
+209
-0
MrpResult.java
src/main/java/com/aps/poa/data/MrpResult.java
+26
-0
OpDef.java
src/main/java/com/aps/poa/data/OpDef.java
+18
-0
OrderDef.java
src/main/java/com/aps/poa/data/OrderDef.java
+2
-12
PurchaseRequirement.java
src/main/java/com/aps/poa/data/PurchaseRequirement.java
+23
-0
ScheduledReceipt.java
src/main/java/com/aps/poa/data/ScheduledReceipt.java
+21
-0
POAOrToolsModel.java
src/main/java/com/aps/poa/model/POAOrToolsModel.java
+17
-0
ObjectiveBuilder.java
src/main/java/com/aps/poa/objective/ObjectiveBuilder.java
+17
-3
RoutingDataService.java
...in/java/com/aps/service/Algorithm/RoutingDataService.java
+171
-129
POABatchSplitTest.java
src/test/java/com/aps/demo/POABatchSplitTest.java
+145
-0
POAMacroPlanTest.java
src/test/java/com/aps/demo/POAMacroPlanTest.java
+0
-157
No files found.
mp/result/optimization_result_1.json
View file @
bf94442c
This diff is collapsed.
Click to expand it.
src/main/java/com/aps/demo/POAMacroPlanTest.java
View file @
bf94442c
This diff is collapsed.
Click to expand it.
src/main/java/com/aps/entity/basic/Entry.java
View file @
bf94442c
...
...
@@ -81,6 +81,28 @@ public class Entry {
* 数量
*/
private
double
quantity
;
/**
* 标准批量(工艺路线工序 batchQty),作为订单拆分时的每批目标大小
*/
private
Double
batchQty
;
/**
* 拆分最小量(拆分批数量的下界)
*/
private
Double
splitMinQty
;
/**
* 拆分最大量(拆分批数量的上界)
*/
private
Double
splitMaxQty
;
/**
* 最大生产量(超过则触发拆分)
*/
private
Double
maxProductionQty
;
/**
* 订单是否可拆分
*/
private
boolean
canSplit
=
false
;
/**
* 工序顺序
*/
...
...
@@ -113,10 +135,10 @@ public class Entry {
*/
private
Integer
state
;
/**
* 是否可中断,间缝插针
*/
private
Long
isInterrupt
=
1
l
;
/**
* 是否可中断,间缝插针
*/
private
Long
isInterrupt
=
1
l
;
/**
* 所需物料
...
...
src/main/java/com/aps/poa/constraint/MaterialInventoryConstraint.java
View file @
bf94442c
This diff is collapsed.
Click to expand it.
src/main/java/com/aps/poa/data/BatchSplitter.java
0 → 100644
View file @
bf94442c
package
com
.
aps
.
poa
.
data
;
import
com.aps.entity.Algorithm.OperationDependency
;
import
com.aps.entity.Algorithm.OrderMaterialRequirement
;
import
com.aps.entity.basic.Entry
;
import
com.aps.entity.basic.MachineOption
;
import
org.springframework.beans.BeanUtils
;
import
java.util.ArrayList
;
import
java.util.Collections
;
import
java.util.HashMap
;
import
java.util.HashSet
;
import
java.util.LinkedHashMap
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.Set
;
import
java.util.stream.Collectors
;
/**
* 订单级标准批量拆分器(POA CP-SAT 前置处理)。
*
* <p>拆分规则:</p>
* <pre>
* 触发(全部满足):
* canSplit == true
* && quantity > maxProductionQty // 超过最大生产量
* && quantity > batchQty // 超过标准批量
*
* 每批目标大小 = batchQty,clamp 到 [splitMinQty, splitMaxQty];
* 批次数 N = ceil(quantity / 目标大小),前 N-1 批 = 目标大小,尾批 = 余量;
* 尾批 < splitMinQty 时并入前一批(合并后仍 ≤ splitMaxQty)。
* </pre>
*
* <p>每批复制整条工艺路线生成子订单(子 orderId = 原orderId_b1/_b2…),
* 加工时长与 BOM 消耗数量按 批数量/原订单数量 比例缩放(暂不考虑 productionTakt)。</p>
*/
public
final
class
BatchSplitter
{
private
BatchSplitter
()
{
}
/**
* 计算订单拆分后的每批数量。
*
* @param quantity 订单生产数量
* @param batchQty 标准批量(每批目标大小)
* @param splitMinQty 拆分最小量
* @param splitMaxQty 拆分最大量
* @param maxProductionQty 最大生产量
* @param canSplit 订单是否可拆分
* @return 每批数量列表;不拆分时返回单元素列表 [quantity]
*/
public
static
List
<
Double
>
splitQuantities
(
double
quantity
,
Double
batchQty
,
Double
splitMinQty
,
Double
splitMaxQty
,
Double
maxProductionQty
,
boolean
canSplit
)
{
double
bq
=
nz
(
batchQty
);
//标准批量
double
smin
=
nz
(
splitMinQty
);
//拆分最小量
double
smax
=
nz
(
splitMaxQty
);
//拆分最大量
double
mpq
=
nz
(
maxProductionQty
);
//最大生产量
// 不拆分:不可拆 / 未超过最大生产量 / 未超过标准批量
if
(!
canSplit
||
quantity
<=
mpq
)
{
return
Collections
.
singletonList
(
quantity
);
}
// 每批目标大小,clamp 到 [splitMinQty, splitMaxQty]
double
target
=
bq
;
if
(
smin
>
0
&&
target
<
smin
)
target
=
smin
;
if
(
smax
>
0
&&
target
>
smax
)
target
=
smax
;
if
(
target
<=
0
)
{
return
Collections
.
singletonList
(
quantity
);
}
int
n
=
(
int
)
Math
.
ceil
(
quantity
/
target
);
if
(
n
<=
1
)
{
return
Collections
.
singletonList
(
quantity
);
}
List
<
Double
>
batches
=
new
ArrayList
<>();
double
remaining
=
quantity
;
for
(
int
i
=
0
;
i
<
n
-
1
;
i
++)
{
batches
.
add
(
target
);
remaining
-=
target
;
}
double
tail
=
remaining
;
// ∈ (0, target]
if
(
smin
>
0
&&
tail
<
smin
)
{
// 尾批不足拆分最小量:并入前一批(合并后仍 ≤ splitMaxQty)
double
lastFull
=
batches
.
get
(
batches
.
size
()
-
1
);
if
(
smax
<=
0
||
lastFull
+
tail
<=
smax
)
{
batches
.
set
(
batches
.
size
()
-
1
,
lastFull
+
tail
);
}
else
{
batches
.
add
(
tail
);
}
}
else
{
batches
.
add
(
tail
);
}
return
batches
;
}
/**
* 对订单列表做标准批量拆分:按 orderId 分组,对满足拆分条件的订单复制整条工艺路线,
* 生成多个子订单;不满足条件的订单原样返回。
*
* @param entries 原始工序列表(每道工序对应一个 Entry)
* @return 拆分后的工序列表(子订单的 orderId 后缀 _b1/_b2…,state=1,splitSourceId 指向原始工序 id)
*/
public
static
List
<
Entry
>
split
(
List
<
Entry
>
entries
)
{
if
(
entries
==
null
||
entries
.
isEmpty
())
{
return
entries
;
}
Map
<
String
,
List
<
Entry
>>
byOrder
=
entries
.
stream
()
.
filter
(
e
->
e
.
getOrderId
()
!=
null
&&
!
e
.
getOrderId
().
isEmpty
())
.
collect
(
Collectors
.
groupingBy
(
Entry:
:
getOrderId
,
LinkedHashMap:
:
new
,
Collectors
.
toList
()));
// 拆分产生的新工序 id 从现有最大 id 之后开始,避免冲突
int
nextId
=
entries
.
stream
().
mapToInt
(
Entry:
:
getId
).
max
().
orElse
(
0
)
+
1
;
List
<
Entry
>
result
=
new
ArrayList
<>();
Set
<
String
>
processed
=
new
HashSet
<>();
for
(
Entry
e
:
entries
)
{
String
orderId
=
e
.
getOrderId
();
if
(
orderId
==
null
||
orderId
.
isEmpty
())
{
result
.
add
(
e
);
continue
;
}
if
(
processed
.
contains
(
orderId
))
{
continue
;
}
processed
.
add
(
orderId
);
List
<
Entry
>
orderOps
=
byOrder
.
get
(
orderId
);
orderOps
.
sort
((
a
,
b
)
->
Integer
.
compare
(
a
.
getSequence
(),
b
.
getSequence
()));
// 订单数量取各工序数量中的最大值(末工序产出量)
double
quantity
=
orderOps
.
stream
()
.
mapToDouble
(
Entry:
:
getQuantity
)
.
filter
(
q
->
q
>
0
)
.
max
()
.
orElse
(
0
);
Entry
first
=
orderOps
.
get
(
0
);
List
<
Double
>
batches
=
splitQuantities
(
quantity
,
first
.
getBatchQty
(),
first
.
getSplitMinQty
(),
first
.
getSplitMaxQty
(),
first
.
getMaxProductionQty
(),
first
.
isCanSplit
());
if
(
batches
.
size
()
<=
1
)
{
result
.
addAll
(
orderOps
);
continue
;
}
for
(
int
b
=
0
;
b
<
batches
.
size
();
b
++)
{
double
batchQty
=
batches
.
get
(
b
);
String
subOrderId
=
orderId
+
"_b"
+
(
b
+
1
);
double
scale
=
quantity
>
0
?
batchQty
/
quantity
:
1
;
// 原工序 id → 新工序 id
Map
<
Integer
,
Integer
>
idMap
=
new
HashMap
<>();
for
(
Entry
op
:
orderOps
)
{
idMap
.
put
(
op
.
getId
(),
nextId
++);
}
for
(
Entry
op
:
orderOps
)
{
result
.
add
(
cloneForBatch
(
op
,
idMap
.
get
(
op
.
getId
()),
subOrderId
,
batchQty
,
scale
,
idMap
));
}
}
}
return
result
;
}
private
static
Entry
cloneForBatch
(
Entry
src
,
int
newId
,
String
subOrderId
,
double
batchQty
,
double
scale
,
Map
<
Integer
,
Integer
>
idMap
)
{
Entry
c
=
new
Entry
();
BeanUtils
.
copyProperties
(
src
,
c
);
c
.
setId
(
newId
);
c
.
setOrderId
(
subOrderId
);
c
.
setQuantity
(
batchQty
);
c
.
setState
(
1
);
// 拆分
c
.
setSplitSourceId
(
src
.
getId
());
c
.
setMinProcessingTime
(
src
.
getMinProcessingTime
()
*
scale
);
c
.
setPrevEntryIds
(
remapDeps
(
src
.
getPrevEntryIds
(),
idMap
));
c
.
setNextEntryIds
(
remapDeps
(
src
.
getNextEntryIds
(),
idMap
));
c
.
setMachineOptions
(
scaleMachineOptions
(
src
.
getMachineOptions
(),
scale
));
c
.
setMaterialRequirements
(
scaleMaterialRequirements
(
src
.
getMaterialRequirements
(),
scale
));
return
c
;
}
/** 前/后序依赖重映射:仅重映射当前订单内部工序的 id,跨订单依赖保持原样 */
private
static
List
<
OperationDependency
>
remapDeps
(
List
<
OperationDependency
>
deps
,
Map
<
Integer
,
Integer
>
idMap
)
{
if
(
deps
==
null
||
deps
.
isEmpty
())
{
return
new
ArrayList
<>();
}
List
<
OperationDependency
>
out
=
new
ArrayList
<>();
for
(
OperationDependency
d
:
deps
)
{
OperationDependency
nd
=
new
OperationDependency
();
BeanUtils
.
copyProperties
(
d
,
nd
);
Integer
np
=
idMap
.
get
(
d
.
getPrevOperationId
());
if
(
np
!=
null
)
nd
.
setPrevOperationId
(
np
);
Integer
nn
=
idMap
.
get
(
d
.
getNextOperationId
());
if
(
nn
!=
null
)
nd
.
setNextOperationId
(
nn
);
out
.
add
(
nd
);
}
return
out
;
}
/** 设备选项:加工时长按批量比例缩放(准备/换型时间不随批量变化) */
private
static
List
<
MachineOption
>
scaleMachineOptions
(
List
<
MachineOption
>
mos
,
double
scale
)
{
if
(
mos
==
null
||
mos
.
isEmpty
())
{
return
new
ArrayList
<>();
}
List
<
MachineOption
>
out
=
new
ArrayList
<>();
for
(
MachineOption
mo
:
mos
)
{
MachineOption
nm
=
new
MachineOption
();
BeanUtils
.
copyProperties
(
mo
,
nm
);
nm
.
setProcessingTime
(
mo
.
getProcessingTime
()
*
scale
);
out
.
add
(
nm
);
}
return
out
;
}
/** BOM 物料需求:需求数量按批量比例缩放(childOrderId 暂不拆分) */
private
static
List
<
OrderMaterialRequirement
>
scaleMaterialRequirements
(
List
<
OrderMaterialRequirement
>
reqs
,
double
scale
)
{
if
(
reqs
==
null
||
reqs
.
isEmpty
())
{
return
new
ArrayList
<>();
}
List
<
OrderMaterialRequirement
>
out
=
new
ArrayList
<>();
for
(
OrderMaterialRequirement
omr
:
reqs
)
{
OrderMaterialRequirement
nr
=
new
OrderMaterialRequirement
();
BeanUtils
.
copyProperties
(
omr
,
nr
);
nr
.
setRequiredQuantity
(
omr
.
getRequiredQuantity
()
*
scale
);
out
.
add
(
nr
);
}
return
out
;
}
private
static
double
nz
(
Double
v
)
{
return
v
==
null
?
0
:
v
;
}
}
src/main/java/com/aps/poa/data/DataAdapter.java
View file @
bf94442c
...
...
@@ -7,6 +7,7 @@ import com.aps.entity.basic.Holiday;
import
com.aps.entity.basic.Machine
;
import
com.aps.entity.basic.MachineOption
;
import
com.aps.entity.basic.MaintenanceWindow
;
import
com.aps.entity.basic.MaterialSupply
;
import
com.aps.entity.basic.SegmentType
;
import
com.aps.entity.basic.TimeSegment
;
...
...
@@ -341,4 +342,23 @@ public final class DataAdapter {
}
return
boms
;
}
/**
* 在途物料 MaterialSupply → 分时段到货 ScheduledReceipt。
* ArrivalTime 相对 timeZero 换算为分钟偏移;timeZero 或 ArrivalTime 为空时视为立即到货(t=0)。
*/
public
static
List
<
ScheduledReceipt
>
toScheduledReceipts
(
List
<
MaterialSupply
>
inTransit
,
LocalDateTime
timeZero
)
{
List
<
ScheduledReceipt
>
receipts
=
new
ArrayList
<>();
if
(
inTransit
==
null
)
return
receipts
;
for
(
MaterialSupply
ms
:
inTransit
)
{
if
(
ms
==
null
||
ms
.
getQuantity
()
<=
0
)
continue
;
double
minute
=
0.0
;
if
(
ms
.
getArrivalTime
()
!=
null
&&
timeZero
!=
null
)
{
minute
=
Math
.
max
(
0
,
ChronoUnit
.
MINUTES
.
between
(
timeZero
,
ms
.
getArrivalTime
()));
}
receipts
.
add
(
new
ScheduledReceipt
(
minute
,
ms
.
getQuantity
()));
}
return
receipts
;
}
}
src/main/java/com/aps/poa/data/InventoryPolicy.java
View file @
bf94442c
package
com
.
aps
.
poa
.
data
;
import
java.util.Collections
;
import
java.util.List
;
/**
* 库存策略:时变库存模型的物料参数。
*/
...
...
@@ -9,13 +12,15 @@ public class InventoryPolicy {
public
final
double
safetyStock
;
public
final
double
minStock
;
public
final
double
maxStock
;
public
final
double
scheduledReceipts
;
/** 在途到货(分时段):每笔带各自到货时间(相对 timeZero 的分钟偏移) */
public
final
List
<
ScheduledReceipt
>
scheduledReceipts
;
public
final
double
lotSize
;
public
final
int
maxPurchaseLotsPerBucket
;
public
final
long
holdingCostPerUnitPerBucket
;
public
InventoryPolicy
(
String
materialId
,
double
onHand
,
double
safetyStock
,
double
minStock
,
double
maxStock
,
double
scheduledReceipts
,
double
minStock
,
double
maxStock
,
List
<
ScheduledReceipt
>
scheduledReceipts
,
double
lotSize
,
int
maxPurchaseLotsPerBucket
,
long
holdingCostPerUnitPerBucket
)
{
if
(
minStock
>
safetyStock
||
safetyStock
>
maxStock
)
{
...
...
@@ -27,12 +32,24 @@ public class InventoryPolicy {
this
.
safetyStock
=
safetyStock
;
this
.
minStock
=
minStock
;
this
.
maxStock
=
maxStock
;
this
.
scheduledReceipts
=
scheduledReceipts
;
this
.
scheduledReceipts
=
scheduledReceipts
!=
null
?
scheduledReceipts
:
Collections
.
emptyList
()
;
this
.
lotSize
=
lotSize
;
this
.
maxPurchaseLotsPerBucket
=
maxPurchaseLotsPerBucket
;
this
.
holdingCostPerUnitPerBucket
=
holdingCostPerUnitPerBucket
;
}
/** 兼容旧接口:单一在途总量 → 视为 t=0 到货的一笔 */
public
InventoryPolicy
(
String
materialId
,
double
onHand
,
double
safetyStock
,
double
minStock
,
double
maxStock
,
double
scheduledReceipts
,
double
lotSize
,
int
maxPurchaseLotsPerBucket
,
long
holdingCostPerUnitPerBucket
)
{
this
(
materialId
,
onHand
,
safetyStock
,
minStock
,
maxStock
,
scheduledReceipts
>
0
?
Collections
.
singletonList
(
new
ScheduledReceipt
(
0
,
scheduledReceipts
))
:
Collections
.<
ScheduledReceipt
>
emptyList
(),
lotSize
,
maxPurchaseLotsPerBucket
,
holdingCostPerUnitPerBucket
);
}
public
InventoryPolicy
(
String
materialId
,
double
onHand
,
double
safetyStock
,
double
minStock
,
double
maxStock
,
double
scheduledReceipts
,
double
lotSize
)
{
...
...
src/main/java/com/aps/poa/data/ModelConfig.java
View file @
bf94442c
...
...
@@ -22,6 +22,8 @@ public class ModelConfig {
public
double
weightHoldingCost
=
1.0
;
/** 安全库存短缺惩罚权重(宏排产) */
public
double
weightSafetyShortfall
=
1000.0
;
/** 超产惩罚权重(宏排产):生产量超出需求时按超出量惩罚,驱动按需生产;与安全库存短缺同量级(超产/短缺 1:1 权衡) */
public
double
weightOverProduction
=
1000.0
;
public
double
maxHorizon
=
10000.0
;
/** 当前排产时刻(相对 timeZero 的分钟偏移):所有工序 start ≥ now 硬下界,防止排到过去 */
public
double
now
=
0.0
;
...
...
src/main/java/com/aps/poa/data/MrpExploder.java
0 → 100644
View file @
bf94442c
package
com
.
aps
.
poa
.
data
;
import
java.util.ArrayList
;
import
java.util.Comparator
;
import
java.util.HashMap
;
import
java.util.HashSet
;
import
java.util.Iterator
;
import
java.util.LinkedHashMap
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.Set
;
import
java.util.TreeMap
;
/**
* MRP 展开器:从成品订单出发,沿 BOM 递归展开,自动生成半成品订单(倒排)与原材料/采购件采购需求。
*
* <p>参照 FjspCpSatDemo 的 MrpEngine 写法,分时段净需求:</p>
* <pre>
* Available(t) = OnHand(0) + Σ ScheduledReceipt(arrivalMinute ≤ t)
* Net = max(0, Gross + SafetyStock − Available(requiredDate))
* Planned = ceilToLot(Net, lotSize)
* </pre>
* <ul>
* <li>半成品(SEMI_FINISHED)Net > 0:自动生成一个 BACKWARD 子订单,绑定到父订单的消耗工序;</li>
* <li>原材料/采购件(RAW/PURCHASED)Net > 0:生成采购需求 {@link PurchaseRequirement}。</li>
* </ul>
*/
public
final
class
MrpExploder
{
private
final
Map
<
String
,
MaterialDef
>
materials
;
private
final
Map
<
String
,
InventoryPolicy
>
policies
;
private
final
Map
<
String
,
List
<
BomItemDef
>>
bomByParent
=
new
HashMap
<>();
private
final
Map
<
String
,
List
<
OpDef
>>
routingTemplates
;
/** 每个物料的可用供给时间线:到货时间 → 数量(按时间升序),含现有库存 + 在途 + 已计划补货 */
private
final
Map
<
String
,
TreeMap
<
Double
,
Double
>>
supplyByMaterial
=
new
HashMap
<>();
private
final
MrpResult
result
=
new
MrpResult
();
private
int
nextJobId
=
1
;
private
MrpExploder
(
Map
<
String
,
MaterialDef
>
materials
,
Map
<
String
,
InventoryPolicy
>
policies
,
List
<
BomItemDef
>
bomItems
,
Map
<
String
,
List
<
OpDef
>>
routingTemplates
)
{
this
.
materials
=
materials
;
this
.
policies
=
policies
;
this
.
routingTemplates
=
routingTemplates
;
for
(
InventoryPolicy
policy
:
policies
.
values
())
{
TreeMap
<
Double
,
Double
>
supply
=
new
TreeMap
<>();
if
(
policy
.
onHand
>
1
e
-
9
)
{
supply
.
merge
(
0.0
,
policy
.
onHand
,
Double:
:
sum
);
}
for
(
ScheduledReceipt
sr
:
policy
.
scheduledReceipts
)
{
supply
.
merge
(
sr
.
arrivalMinute
,
sr
.
quantity
,
Double:
:
sum
);
}
supplyByMaterial
.
put
(
policy
.
materialId
,
supply
);
}
for
(
BomItemDef
item
:
bomItems
)
{
bomByParent
.
computeIfAbsent
(
item
.
parentMaterialId
,
k
->
new
ArrayList
<>()).
add
(
item
);
}
}
public
static
MrpResult
explode
(
List
<
OrderDef
>
finishedOrders
,
Map
<
String
,
MaterialDef
>
materials
,
Map
<
String
,
InventoryPolicy
>
policies
,
List
<
BomItemDef
>
bomItems
,
Map
<
String
,
List
<
OpDef
>>
routingTemplates
)
{
MrpExploder
engine
=
new
MrpExploder
(
materials
,
policies
,
bomItems
,
routingTemplates
);
List
<
OrderDef
>
sorted
=
new
ArrayList
<>(
finishedOrders
);
sorted
.
sort
(
Comparator
.
comparingDouble
(
o
->
o
.
due
));
for
(
OrderDef
order
:
sorted
)
{
if
(
order
.
materialId
==
null
||
order
.
quantity
<=
0
)
continue
;
engine
.
explodeOrder
(
order
,
new
HashSet
<>());
}
return
engine
.
result
;
}
private
void
explodeOrder
(
OrderDef
parent
,
Set
<
String
>
path
)
{
if
(!
path
.
add
(
parent
.
materialId
))
{
throw
new
IllegalArgumentException
(
"检测到循环 BOM: "
+
path
+
" -> "
+
parent
.
materialId
);
}
for
(
BomItemDef
item
:
bomByParent
.
getOrDefault
(
parent
.
materialId
,
new
ArrayList
<>()))
{
MaterialDef
child
=
materials
.
get
(
item
.
componentMaterialId
);
if
(
child
==
null
)
{
throw
new
IllegalArgumentException
(
"BOM 引用的物料不存在: "
+
item
.
componentMaterialId
);
}
InventoryPolicy
policy
=
policies
.
get
(
child
.
id
);
if
(
policy
==
null
)
{
throw
new
IllegalArgumentException
(
"缺少库存策略: "
+
child
.
id
);
}
double
gross
=
parent
.
quantity
*
item
.
qtyPer
;
double
requiredDate
=
Math
.
max
(
0
,
parent
.
due
-
item
.
supplyLeadTime
);
double
available
=
availableBefore
(
child
.
id
,
requiredDate
);
double
targetStock
=
Math
.
max
(
policy
.
safetyStock
,
policy
.
minStock
);
double
net
=
Math
.
max
(
0.0
,
gross
+
targetStock
-
available
);
double
planned
=
ceilToLot
(
net
,
policy
.
lotSize
);
// 消耗按到货时间先后进行,补货在 requiredDate 到货供后续更晚需求使用
consume
(
child
.
id
,
requiredDate
,
gross
);
if
(
planned
>
0
)
{
addSupply
(
child
.
id
,
requiredDate
,
planned
);
}
if
(
planned
>
0
&&
child
.
type
==
MaterialDef
.
MaterialType
.
SEMI_FINISHED
)
{
OrderDef
childOrder
=
createManufacturingOrder
(
parent
,
item
,
child
,
planned
,
requiredDate
);
result
.
generatedOrders
.
add
(
childOrder
);
explodeOrder
(
childOrder
,
new
HashSet
<>(
path
));
}
else
if
(
planned
>
0
&&
(
child
.
type
==
MaterialDef
.
MaterialType
.
RAW
||
child
.
type
==
MaterialDef
.
MaterialType
.
PURCHASED
))
{
result
.
purchases
.
add
(
new
PurchaseRequirement
(
child
.
id
,
planned
,
requiredDate
));
}
}
path
.
remove
(
parent
.
materialId
);
}
private
OrderDef
createManufacturingOrder
(
OrderDef
parent
,
BomItemDef
bomItem
,
MaterialDef
material
,
double
quantity
,
double
due
)
{
List
<
OpDef
>
templates
=
routingTemplates
.
get
(
material
.
id
);
if
(
templates
==
null
||
templates
.
isEmpty
())
{
throw
new
IllegalArgumentException
(
"自制物料缺少工艺模板: "
+
material
.
id
);
}
String
childOrderId
=
"MRP_"
+
(
nextJobId
++);
// 1. 建立模板工序 id → 新工序 id 映射
Map
<
String
,
String
>
idMap
=
new
HashMap
<>();
for
(
OpDef
t
:
templates
)
{
idMap
.
put
(
t
.
id
,
childOrderId
+
"_"
+
t
.
id
);
}
result
.
operationIdMaps
.
put
(
childOrderId
,
new
LinkedHashMap
<>(
idMap
));
// 2. 复制工艺模板,生成新工序(predecessors 经 idMap 重映射)
List
<
OpDef
>
childOps
=
new
ArrayList
<>();
for
(
OpDef
t
:
templates
)
{
OpDef
.
Builder
b
=
OpDef
.
builder
(
idMap
.
get
(
t
.
id
),
t
.
name
);
for
(
int
i
=
0
;
i
<
t
.
candidateResources
.
size
();
i
++)
{
b
.
addResource
(
t
.
candidateResources
.
get
(
i
),
t
.
durations
.
get
(
i
));
}
b
.
orderId
(
childOrderId
)
.
interruptible
(
t
.
interruptible
)
.
setupTime
(
t
.
setupTime
)
.
maxWaitAfter
(
t
.
maxWaitAfter
)
.
unplannedCost
(
t
.
unplannedCost
)
.
minProductionQty
(
t
.
minProductionQty
)
.
maxProductionQty
(
t
.
maxProductionQty
)
.
lotMultiplier
(
t
.
lotMultiplier
);
if
(
t
.
bufferId
!=
null
)
{
b
.
buffer
(
t
.
bufferId
,
t
.
bufferCapacity
);
}
for
(
String
prevId
:
t
.
predecessors
.
keySet
())
{
String
newPrev
=
idMap
.
get
(
prevId
);
if
(
newPrev
!=
null
)
b
.
predecessor
(
newPrev
);
}
childOps
.
add
(
b
.
build
());
}
result
.
generatedOps
.
addAll
(
childOps
);
String
firstOpId
=
childOps
.
get
(
0
).
id
;
String
lastOpId
=
childOps
.
get
(
childOps
.
size
()
-
1
).
id
;
return
new
OrderDef
(
childOrderId
,
firstOpId
,
lastOpId
,
-
1
,
due
,
material
.
id
,
quantity
,
OrderDef
.
PlanDirection
.
BACKWARD
,
parent
.
orderId
,
bomItem
.
consumeAtOperationId
,
bomItem
.
supplyLeadTime
);
}
/** 需求时间点前(含)已到货的可用量 */
private
double
availableBefore
(
String
materialId
,
double
time
)
{
TreeMap
<
Double
,
Double
>
supply
=
supplyByMaterial
.
get
(
materialId
);
if
(
supply
==
null
)
return
0.0
;
double
sum
=
0.0
;
for
(
double
q
:
supply
.
headMap
(
time
,
true
).
values
())
{
sum
+=
q
;
}
return
sum
;
}
/** 按到货时间先后消耗供给(只消耗 time ≤ requiredDate 的部分) */
private
void
consume
(
String
materialId
,
double
requiredDate
,
double
qty
)
{
if
(
qty
<=
1
e
-
9
)
return
;
TreeMap
<
Double
,
Double
>
supply
=
supplyByMaterial
.
get
(
materialId
);
if
(
supply
==
null
)
return
;
double
remaining
=
qty
;
Iterator
<
Map
.
Entry
<
Double
,
Double
>>
it
=
supply
.
headMap
(
requiredDate
,
true
).
entrySet
().
iterator
();
while
(
it
.
hasNext
()
&&
remaining
>
1
e
-
9
)
{
Map
.
Entry
<
Double
,
Double
>
e
=
it
.
next
();
double
take
=
Math
.
min
(
remaining
,
e
.
getValue
());
remaining
-=
take
;
double
left
=
e
.
getValue
()
-
take
;
if
(
left
<=
1
e
-
9
)
{
it
.
remove
();
}
else
{
e
.
setValue
(
left
);
}
}
}
private
void
addSupply
(
String
materialId
,
double
time
,
double
qty
)
{
if
(
qty
<=
1
e
-
9
)
return
;
supplyByMaterial
.
computeIfAbsent
(
materialId
,
k
->
new
TreeMap
<>())
.
merge
(
time
,
qty
,
Double:
:
sum
);
}
private
static
double
ceilToLot
(
double
value
,
double
lotSize
)
{
if
(
value
<=
1
e
-
9
)
return
0.0
;
return
Math
.
ceil
((
value
-
1
e
-
9
)
/
lotSize
)
*
lotSize
;
}
}
src/main/java/com/aps/poa/data/MrpResult.java
0 → 100644
View file @
bf94442c
package
com
.
aps
.
poa
.
data
;
import
java.util.ArrayList
;
import
java.util.LinkedHashMap
;
import
java.util.List
;
import
java.util.Map
;
/**
* MRP 展开结果:自动生成的半成品订单/工序,以及原材料/采购件的采购需求。
*/
public
class
MrpResult
{
/** 生成的半成品工序(含成品工序之外的工序,orderId 已指向半成品订单) */
public
final
List
<
OpDef
>
generatedOps
=
new
ArrayList
<>();
/** 生成的半成品订单(planDirection=BACKWARD,含父订单/父工序/提前期) */
public
final
List
<
OrderDef
>
generatedOrders
=
new
ArrayList
<>();
/** 原材料/采购件的净采购需求 */
public
final
List
<
PurchaseRequirement
>
purchases
=
new
ArrayList
<>();
/** 每个生成订单的工序映射:orderId -> (模板工序 id -> 实例工序 id) */
public
final
Map
<
String
,
Map
<
String
,
String
>>
operationIdMaps
=
new
LinkedHashMap
<>();
@Override
public
String
toString
()
{
return
String
.
format
(
"MrpResult{半成品订单=%d, 半成品工序=%d, 采购需求=%d}"
,
generatedOrders
.
size
(),
generatedOps
.
size
(),
purchases
.
size
());
}
}
src/main/java/com/aps/poa/data/OpDef.java
View file @
bf94442c
...
...
@@ -41,6 +41,12 @@ public class OpDef {
public
final
String
bufferId
;
/** WIP 缓冲区容量(同时最多等待的工件数),bufferId 非空时有效 */
public
final
int
bufferCapacity
;
/** 最小生产量(工艺级,0 表示不额外约束,实际下界为 max(需求, 此值)) */
public
final
double
minProductionQty
;
/** 最大生产量(工艺级,0 表示不额外约束) */
public
final
double
maxProductionQty
;
/** 批量倍数(工单数量必须是该数值的整数倍,1 表示无批量约束) */
public
final
double
lotMultiplier
;
private
OpDef
(
Builder
b
)
{
this
.
id
=
b
.
id
;
...
...
@@ -60,6 +66,9 @@ public class OpDef {
this
.
maxWaitAfter
=
b
.
maxWaitAfter
;
this
.
bufferId
=
b
.
bufferId
;
this
.
bufferCapacity
=
b
.
bufferCapacity
;
this
.
minProductionQty
=
b
.
minProductionQty
;
this
.
maxProductionQty
=
b
.
maxProductionQty
;
this
.
lotMultiplier
=
b
.
lotMultiplier
;
}
public
static
Builder
builder
(
String
id
,
String
name
)
{
...
...
@@ -89,6 +98,12 @@ public class OpDef {
String
bufferId
=
null
;
int
bufferCapacity
=
0
;
double
minProductionQty
;
/** 最大生产量(工艺级,0 表示不额外约束) */
double
maxProductionQty
;
/** 批量倍数(工单数量必须是该数值的整数倍,1 表示无批量约束) */
double
lotMultiplier
=
1
;
Builder
(
String
id
,
String
name
)
{
this
.
id
=
id
;
this
.
name
=
name
;
...
...
@@ -122,6 +137,9 @@ public class OpDef {
public
Builder
buffer
(
String
bufferId
,
int
capacity
)
{
this
.
bufferId
=
bufferId
;
this
.
bufferCapacity
=
capacity
;
return
this
;
}
public
Builder
minProductionQty
(
double
v
)
{
this
.
minProductionQty
=
v
;
return
this
;
}
public
Builder
maxProductionQty
(
double
v
)
{
this
.
maxProductionQty
=
v
;
return
this
;
}
public
Builder
lotMultiplier
(
double
v
)
{
this
.
lotMultiplier
=
v
;
return
this
;
}
public
OpDef
build
()
{
if
(
candidateResources
.
isEmpty
())
{
...
...
src/main/java/com/aps/poa/data/OrderDef.java
View file @
bf94442c
...
...
@@ -7,7 +7,7 @@ package com.aps.poa.data;
* 分别指向该订单的首/末工序。</p>
*
* <p>宏排产字段(可选):{@link #materialId} 表示该订单生产的物料,
* {@link #
maxProductionQty}/{@link #productionLotSize} 让生产数量成为 CP-SAT 决策变量
;
* {@link #
quantity} 为需求量;生产批量/最小最大生产量定义在工艺工序(OpDef)上
;
* {@link #planDirection} 支持成品正排 + 半成品倒排。</p>
*/
public
class
OrderDef
{
...
...
@@ -27,12 +27,6 @@ public class OrderDef {
public
final
String
materialId
;
/** 需求量 */
public
final
double
quantity
;
/** 最小生产量(≥ quantity) */
public
final
double
minProductionQty
;
/** 最大生产量(受最大库存限制) */
public
final
double
maxProductionQty
;
/** 生产批量 */
public
final
double
productionLotSize
;
/** 排程方向(成品正排 / 半成品倒排) */
public
final
PlanDirection
planDirection
;
/** 父订单 ID(半成品归属的成品订单) */
...
...
@@ -45,14 +39,13 @@ public class OrderDef {
public
OrderDef
(
String
orderId
,
String
firstOpId
,
String
lastOpId
,
double
release
,
double
due
)
{
this
(
orderId
,
firstOpId
,
lastOpId
,
release
,
due
,
null
,
0
,
0
,
0
,
0
,
PlanDirection
.
FORWARD
,
null
,
0
,
PlanDirection
.
FORWARD
,
null
,
null
,
0
);
}
public
OrderDef
(
String
orderId
,
String
firstOpId
,
String
lastOpId
,
double
release
,
double
due
,
String
materialId
,
double
quantity
,
double
minProductionQty
,
double
maxProductionQty
,
double
productionLotSize
,
PlanDirection
planDirection
,
String
parentOrderId
,
String
parentOperationId
,
double
supplyLeadTime
)
{
this
.
orderId
=
orderId
;
...
...
@@ -62,9 +55,6 @@ public class OrderDef {
this
.
due
=
due
;
this
.
materialId
=
materialId
;
this
.
quantity
=
quantity
;
this
.
minProductionQty
=
minProductionQty
;
this
.
maxProductionQty
=
maxProductionQty
;
this
.
productionLotSize
=
productionLotSize
;
this
.
planDirection
=
planDirection
;
this
.
parentOrderId
=
parentOrderId
;
this
.
parentOperationId
=
parentOperationId
;
...
...
src/main/java/com/aps/poa/data/PurchaseRequirement.java
0 → 100644
View file @
bf94442c
package
com
.
aps
.
poa
.
data
;
/**
* 采购需求:MRP 展开后对原材料/采购件的净采购建议。
*/
public
class
PurchaseRequirement
{
public
final
String
materialId
;
/** 净采购量(已按 LotSize 取整) */
public
final
double
quantity
;
/** 需求日期(模型分钟):须在该日期前到货 */
public
final
double
requiredDate
;
public
PurchaseRequirement
(
String
materialId
,
double
quantity
,
double
requiredDate
)
{
this
.
materialId
=
materialId
;
this
.
quantity
=
quantity
;
this
.
requiredDate
=
requiredDate
;
}
@Override
public
String
toString
()
{
return
String
.
format
(
"Purchase{material=%s, qty=%.2f, due=%.1f}"
,
materialId
,
quantity
,
requiredDate
);
}
}
src/main/java/com/aps/poa/data/ScheduledReceipt.java
0 → 100644
View file @
bf94442c
package
com
.
aps
.
poa
.
data
;
/**
* 一笔在途到货:在指定到货时间到货的固定数量。
*/
public
class
ScheduledReceipt
{
/** 到货时间:相对 timeZero 的分钟偏移(与工序 start/end 同一时间轴) */
public
final
double
arrivalMinute
;
/** 到货数量 */
public
final
double
quantity
;
public
ScheduledReceipt
(
double
arrivalMinute
,
double
quantity
)
{
this
.
arrivalMinute
=
arrivalMinute
;
this
.
quantity
=
quantity
;
}
@Override
public
String
toString
()
{
return
String
.
format
(
"ScheduledReceipt{at=%.1f, qty=%.2f}"
,
arrivalMinute
,
quantity
);
}
}
src/main/java/com/aps/poa/model/POAOrToolsModel.java
View file @
bf94442c
...
...
@@ -3,6 +3,7 @@ package com.aps.poa.model;
import
com.aps.entity.basic.Entry
;
import
com.aps.entity.basic.Machine
;
import
com.aps.poa.constraint.ConstraintFactory
;
import
com.aps.poa.data.BatchSplitter
;
import
com.aps.poa.data.BomItemDef
;
import
com.aps.poa.data.BomRelation
;
import
com.aps.poa.data.DataAdapter
;
...
...
@@ -104,6 +105,10 @@ public class POAOrToolsModel {
private
final
Map
<
String
,
IntVar
>
productionLotCountVars
=
new
LinkedHashMap
<>();
/** 每个物料的时变库存 Inventory[t](宏排产) */
private
final
Map
<
String
,
IntVar
[]>
inventoryVars
=
new
LinkedHashMap
<>();
/** 每个物料的采购入库 PurchaseReceipt[t](宏排产,供结果输出) */
private
final
Map
<
String
,
IntVar
[]>
purchaseReceiptVars
=
new
LinkedHashMap
<>();
/** 每个物料的采购批次数 PurchaseLots[t](宏排产,供结果输出) */
private
final
Map
<
String
,
IntVar
[]>
purchaseLotCountVars
=
new
LinkedHashMap
<>();
/** 库存持有成本总和(进目标) */
private
IntVar
totalInventoryHoldingCost
;
/** 安全库存短缺总和(进目标) */
...
...
@@ -190,6 +195,16 @@ public class POAOrToolsModel {
List
<
SetupMatrix
>
setupMatrices
,
List
<
InventoryConstraint
>
invConstraints
,
ModelConfig
config
)
{
this
(
BatchSplitter
.
split
(
entries
),
machines
,
setupMatrices
,
invConstraints
,
config
,
true
);
}
/** entries 已完成标准批量拆分,三个适配器复用同一份拆分结果 */
private
POAOrToolsModel
(
List
<
Entry
>
entries
,
List
<
Machine
>
machines
,
List
<
SetupMatrix
>
setupMatrices
,
List
<
InventoryConstraint
>
invConstraints
,
ModelConfig
config
,
boolean
alreadySplit
)
{
this
(
DataAdapter
.
toOpDefs
(
entries
),
DataAdapter
.
toResourceDefs
(
machines
,
config
!=
null
?
config
.
timeZero
:
null
),
DataAdapter
.
toBomRelations
(
entries
),
...
...
@@ -300,6 +315,8 @@ public class POAOrToolsModel {
public
Map
<
String
,
IntVar
>
getProductionQtyVars
()
{
return
productionQtyVars
;
}
public
Map
<
String
,
IntVar
>
getProductionLotCountVars
()
{
return
productionLotCountVars
;
}
public
Map
<
String
,
IntVar
[]>
getInventoryVars
()
{
return
inventoryVars
;
}
public
Map
<
String
,
IntVar
[]>
getPurchaseReceiptVars
()
{
return
purchaseReceiptVars
;
}
public
Map
<
String
,
IntVar
[]>
getPurchaseLotCountVars
()
{
return
purchaseLotCountVars
;
}
public
IntVar
getTotalInventoryHoldingCost
()
{
return
totalInventoryHoldingCost
;
}
public
void
setTotalInventoryHoldingCost
(
IntVar
v
)
{
this
.
totalInventoryHoldingCost
=
v
;
}
public
IntVar
getTotalSafetyStockShortfall
()
{
return
totalSafetyStockShortfall
;
}
...
...
src/main/java/com/aps/poa/objective/ObjectiveBuilder.java
View file @
bf94442c
...
...
@@ -146,15 +146,29 @@ public final class ObjectiveBuilder {
}
}
/** 库存持有成本 + 安全库存短缺(宏排产,进目标) */
/** 库存持有成本 + 安全库存短缺
+ 超产惩罚
(宏排产,进目标) */
private
static
void
addInventoryCostTerms
(
POAOrToolsModel
model
)
{
// 数量类目标变量(库存/短缺/产量)按 QUANTITY_SCALE 缩放,系数需除以 QUANTITY_SCALE 抵消,
// 否则持有成本/短缺惩罚相对未规划惩罚被放大 1000 倍,导致权重失衡(如成品被误判为未规划更划算)
long
holdingCoeff
=
Math
.
max
(
1L
,
model
.
lscale
(
model
.
getConfig
().
weightHoldingCost
)
/
POAOrToolsModel
.
QUANTITY_SCALE
);
long
shortfallCoeff
=
Math
.
max
(
1L
,
model
.
lscale
(
model
.
getConfig
().
weightSafetyShortfall
)
/
POAOrToolsModel
.
QUANTITY_SCALE
);
long
overProdCoeff
=
Math
.
max
(
1L
,
model
.
lscale
(
model
.
getConfig
().
weightOverProduction
)
/
POAOrToolsModel
.
QUANTITY_SCALE
);
if
(
model
.
getTotalInventoryHoldingCost
()
!=
null
)
{
model
.
getObjectiveVars
().
add
(
model
.
getTotalInventoryHoldingCost
());
model
.
getObjectiveCoeffs
().
add
(
model
.
lscale
(
model
.
getConfig
().
weightHoldingCost
)
);
model
.
getObjectiveCoeffs
().
add
(
holdingCoeff
);
}
if
(
model
.
getTotalSafetyStockShortfall
()
!=
null
)
{
model
.
getObjectiveVars
().
add
(
model
.
getTotalSafetyStockShortfall
());
model
.
getObjectiveCoeffs
().
add
(
model
.
lscale
(
model
.
getConfig
().
weightSafetyShortfall
));
model
.
getObjectiveCoeffs
().
add
(
shortfallCoeff
);
}
// 超产惩罚:驱动生产量贴近需求。否则成品不计持有成本时,生产量失去下压动机会任意飘高
for
(
IntVar
qty
:
model
.
getProductionQtyVars
().
values
())
{
model
.
getObjectiveVars
().
add
(
qty
);
model
.
getObjectiveCoeffs
().
add
(
overProdCoeff
);
}
}
}
src/main/java/com/aps/service/Algorithm/RoutingDataService.java
View file @
bf94442c
This diff is collapsed.
Click to expand it.
src/test/java/com/aps/demo/POABatchSplitTest.java
0 → 100644
View file @
bf94442c
package
com
.
aps
.
demo
;
import
com.aps.entity.Algorithm.OperationDependency
;
import
com.aps.entity.basic.Entry
;
import
com.aps.entity.basic.MachineOption
;
import
com.aps.poa.data.BatchSplitter
;
import
org.junit.jupiter.api.Test
;
import
java.util.Arrays
;
import
java.util.List
;
import
java.util.stream.Collectors
;
import
static
org
.
junit
.
jupiter
.
api
.
Assertions
.
assertEquals
;
/**
* 标准批量拆分(BatchSplitter)单元测试。
*/
public
class
POABatchSplitTest
{
// ==================== splitQuantities:纯算法 ====================
@Test
public
void
testNoSplit
()
{
// 不可拆分
assertEquals
(
Arrays
.
asList
(
1000.0
),
BatchSplitter
.
splitQuantities
(
1000
,
500.0
,
100.0
,
600.0
,
800.0
,
false
));
// 未超过最大生产量
assertEquals
(
Arrays
.
asList
(
800.0
),
BatchSplitter
.
splitQuantities
(
800
,
500.0
,
100.0
,
600.0
,
800.0
,
true
));
// 未超过标准批量
assertEquals
(
Arrays
.
asList
(
500.0
),
BatchSplitter
.
splitQuantities
(
500
,
500.0
,
100.0
,
600.0
,
800.0
,
true
));
}
@Test
public
void
testEqualSplit
()
{
assertEquals
(
Arrays
.
asList
(
500.0
,
500.0
),
BatchSplitter
.
splitQuantities
(
1000
,
500.0
,
100.0
,
600.0
,
800.0
,
true
));
}
@Test
public
void
testTailMerge
()
{
// 尾批 1 < splitMinQty(100),并入前一批 → [500, 501]
assertEquals
(
Arrays
.
asList
(
500.0
,
501.0
),
BatchSplitter
.
splitQuantities
(
1001
,
500.0
,
100.0
,
600.0
,
800.0
,
true
));
}
@Test
public
void
testTailKept
()
{
// 尾批 200 ≥ splitMinQty(100),保留 → [500, 500, 200]
assertEquals
(
Arrays
.
asList
(
500.0
,
500.0
,
200.0
),
BatchSplitter
.
splitQuantities
(
1200
,
500.0
,
100.0
,
600.0
,
800.0
,
true
));
}
@Test
public
void
testClampToSplitRange
()
{
// batchQty(150) < splitMinQty(200) → 每批 200
assertEquals
(
Arrays
.
asList
(
200.0
,
200.0
,
200.0
,
200.0
,
200.0
),
BatchSplitter
.
splitQuantities
(
1000
,
150.0
,
200.0
,
400.0
,
800.0
,
true
));
// batchQty(700) > splitMaxQty(400) → 每批 400,尾批 200
assertEquals
(
Arrays
.
asList
(
400.0
,
400.0
,
200.0
),
BatchSplitter
.
splitQuantities
(
1000
,
700.0
,
100.0
,
400.0
,
800.0
,
true
));
}
// ==================== split:Entry 级订单拆分 ====================
@Test
public
void
testEntrySplit
()
{
Entry
e1
=
op
(
1
,
"FG1"
,
1
,
"工序0"
);
Entry
e2
=
op
(
2
,
"FG1"
,
2
,
"工序1"
);
Entry
e3
=
op
(
3
,
"FG1"
,
3
,
"工序2"
);
Entry
e4
=
op
(
4
,
"FG1"
,
4
,
"工序3"
);
e4
.
setProductId
(
"FG1"
);
e4
.
setQuantity
(
1000
);
List
<
Entry
>
order
=
Arrays
.
asList
(
e1
,
e2
,
e3
,
e4
);
for
(
Entry
e
:
order
)
{
e
.
setBatchQty
(
500.0
);
e
.
setSplitMinQty
(
100.0
);
e
.
setSplitMaxQty
(
600.0
);
e
.
setMaxProductionQty
(
800.0
);
e
.
setCanSplit
(
true
);
}
dep
(
e2
,
e1
);
dep
(
e3
,
e2
);
dep
(
e4
,
e3
);
List
<
Entry
>
result
=
BatchSplitter
.
split
(
order
);
// 2 批 → 8 道工序
assertEquals
(
8
,
result
.
size
());
// 两个子订单:FG1_b1 / FG1_b2
List
<
String
>
orderIds
=
result
.
stream
().
map
(
Entry:
:
getOrderId
).
distinct
().
sorted
().
collect
(
Collectors
.
toList
());
assertEquals
(
Arrays
.
asList
(
"FG1_b1"
,
"FG1_b2"
),
orderIds
);
// 每个子订单 4 道工序,数量 = 500
for
(
String
oid
:
orderIds
)
{
List
<
Entry
>
ops
=
result
.
stream
().
filter
(
e
->
oid
.
equals
(
e
.
getOrderId
()))
.
sorted
((
a
,
b
)
->
Integer
.
compare
(
a
.
getSequence
(),
b
.
getSequence
()))
.
collect
(
Collectors
.
toList
());
assertEquals
(
4
,
ops
.
size
());
for
(
Entry
e
:
ops
)
{
assertEquals
(
500.0
,
e
.
getQuantity
());
assertEquals
(
Integer
.
valueOf
(
1
),
e
.
getState
());
}
}
// 子订单内前序依赖已重映射到本批内部工序 id
List
<
Entry
>
b1
=
result
.
stream
().
filter
(
e
->
"FG1_b1"
.
equals
(
e
.
getOrderId
())).
collect
(
Collectors
.
toList
());
Entry
b1Seq2
=
b1
.
stream
().
filter
(
e
->
e
.
getSequence
()
==
2
).
findFirst
()
.
orElseThrow
(()
->
new
IllegalStateException
(
"seq2 not found"
));
assertEquals
(
1
,
b1Seq2
.
getPrevEntryIds
().
size
());
int
prevId
=
b1Seq2
.
getPrevEntryIds
().
get
(
0
).
getPrevOperationId
();
Entry
b1Seq1
=
b1
.
stream
().
filter
(
e
->
e
.
getSequence
()
==
1
).
findFirst
()
.
orElseThrow
(()
->
new
IllegalStateException
(
"seq1 not found"
));
assertEquals
(
b1Seq1
.
getId
(),
prevId
);
}
// ==================== 辅助 ====================
private
static
Entry
op
(
int
id
,
String
orderId
,
int
sequence
,
String
name
)
{
Entry
e
=
new
Entry
();
e
.
setId
(
id
);
e
.
setOrderId
(
orderId
);
e
.
setSequence
(
sequence
);
e
.
setRoutingDetailName
(
name
);
e
.
setPriority
(
100
);
e
.
setMachineOptions
(
Arrays
.
asList
(
opt
(
0L
,
60
)));
return
e
;
}
private
static
MachineOption
opt
(
Long
machineId
,
double
processingTime
)
{
MachineOption
mo
=
new
MachineOption
();
mo
.
setMachineId
(
machineId
);
mo
.
setProcessingTime
(
processingTime
);
return
mo
;
}
private
static
void
dep
(
Entry
entry
,
Entry
prev
)
{
OperationDependency
d
=
new
OperationDependency
();
d
.
setPrevOperationId
(
prev
.
getId
());
entry
.
getPrevEntryIds
().
add
(
d
);
}
}
src/test/java/com/aps/demo/POAMacroPlanTest.java
deleted
100644 → 0
View file @
24eee50e
package
com
.
aps
.
demo
;
import
com.aps.poa.data.BomItemDef
;
import
com.aps.poa.data.BomRelation
;
import
com.aps.poa.data.InventoryConstraint
;
import
com.aps.poa.data.InventoryPolicy
;
import
com.aps.poa.data.MaterialDef
;
import
com.aps.poa.data.ModelConfig
;
import
com.aps.poa.data.OpDef
;
import
com.aps.poa.data.OrderDef
;
import
com.aps.poa.data.ResourceDef
;
import
com.aps.poa.data.SetupMatrix
;
import
com.aps.poa.data.SolveResult
;
import
com.aps.poa.model.POAOrToolsModel
;
import
com.google.ortools.Loader
;
import
com.google.ortools.sat.CpSolver
;
import
com.google.ortools.sat.CpSolverStatus
;
import
com.google.ortools.sat.IntVar
;
import
java.util.ArrayList
;
import
java.util.Arrays
;
import
java.util.List
;
/**
* 宏排产补充功能测试:最大等待时间 maxWait、WIP 缓冲区容量、时变库存+采购+生产数量、混合方向。
* 运行:java -cp <classpath> com.aps.demo.POAMacroPlanTest
*/
public
class
POAMacroPlanTest
{
public
static
void
main
(
String
[]
args
)
{
Loader
.
loadNativeLibraries
();
testMaxWait
();
// testWipCapacity();
testMaterialInventory
();
System
.
out
.
println
(
"\n[PASS] 宏排产补充功能测试全部通过"
);
}
/** 最大等待时间:BACKWARD 下后工序想尽量后推,但受 maxWait 限制 */
static
void
testMaxWait
()
{
List
<
OpDef
>
ops
=
new
ArrayList
<>();
ops
.
add
(
OpDef
.
builder
(
"A"
,
"工序A"
).
addResource
(
"M0"
,
10
).
orderId
(
"J0"
)
.
maxWaitAfter
(
5
).
unplannedCost
(
100000
).
build
());
ops
.
add
(
OpDef
.
builder
(
"B"
,
"工序B"
).
addResource
(
"M1"
,
10
).
orderId
(
"J0"
)
.
predecessor
(
"A"
).
unplannedCost
(
100000
).
build
());
List
<
ResourceDef
>
resources
=
Arrays
.
asList
(
new
ResourceDef
(
"M0"
,
200
),
new
ResourceDef
(
"M1"
,
200
));
ModelConfig
cfg
=
new
ModelConfig
();
cfg
.
maxHorizon
=
200
;
cfg
.
scheduleMode
=
ModelConfig
.
ScheduleMode
.
BACKWARD
;
cfg
.
timeLimitSeconds
=
10
;
POAOrToolsModel
scheduler
=
new
POAOrToolsModel
(
ops
,
resources
,
new
ArrayList
<>(),
new
ArrayList
<>(),
new
ArrayList
<>(),
cfg
);
scheduler
.
build
();
SolveResult
r
=
scheduler
.
solve
();
double
wait
=
r
.
startTimes
.
get
(
"B"
)
-
r
.
endTimes
.
get
(
"A"
);
if
(
wait
>
5
+
1
e
-
6
)
{
throw
new
AssertionError
(
"maxWait 约束违反: B.start - A.end = "
+
wait
+
" > 5"
);
}
System
.
out
.
println
(
"[PASS] maxWait: B.start - A.end = "
+
wait
+
" ≤ 5"
);
}
/** WIP 缓冲区容量:两个工单的半成品在 M0→M1 间等待,缓冲区容量=1 */
static
void
testWipCapacity
()
{
List
<
OpDef
>
ops
=
new
ArrayList
<>();
// J0: A0(M0) -> B0(M1),A0 完成后进入 BUF 等待 B0
ops
.
add
(
OpDef
.
builder
(
"A0"
,
"A0"
).
addResource
(
"M0"
,
20
).
orderId
(
"J0"
)
.
buffer
(
"BUF"
,
1
).
unplannedCost
(
100000
).
build
());
ops
.
add
(
OpDef
.
builder
(
"B0"
,
"B0"
).
addResource
(
"M1"
,
20
).
orderId
(
"J0"
)
.
predecessor
(
"A0"
).
unplannedCost
(
100000
).
build
());
// J1: A1(M0) -> B1(M1)
ops
.
add
(
OpDef
.
builder
(
"A1"
,
"A1"
).
addResource
(
"M0"
,
20
).
orderId
(
"J1"
)
.
buffer
(
"BUF"
,
1
).
unplannedCost
(
100000
).
build
());
ops
.
add
(
OpDef
.
builder
(
"B1"
,
"B1"
).
addResource
(
"M1"
,
20
).
orderId
(
"J1"
)
.
predecessor
(
"A1"
).
unplannedCost
(
100000
).
build
());
List
<
ResourceDef
>
resources
=
Arrays
.
asList
(
new
ResourceDef
(
"M0"
,
200
),
new
ResourceDef
(
"M1"
,
200
));
ModelConfig
cfg
=
new
ModelConfig
();
cfg
.
maxHorizon
=
200
;
cfg
.
timeLimitSeconds
=
10
;
POAOrToolsModel
scheduler
=
new
POAOrToolsModel
(
ops
,
resources
,
new
ArrayList
<>(),
new
ArrayList
<>(),
new
ArrayList
<>(),
cfg
);
scheduler
.
build
();
SolveResult
r
=
scheduler
.
solve
();
if
(!
r
.
isFeasible
||
!
r
.
unscheduledOps
.
isEmpty
())
{
throw
new
AssertionError
(
"WIP 场景应可行且全规划: "
+
r
);
}
System
.
out
.
println
(
"[PASS] WIP 缓冲区容量: makespan="
+
r
.
makespan
+
", 全规划"
);
}
/** 时变库存 + 采购 + 生产数量 + 混合方向(成品正排 + 半成品倒排) */
static
void
testMaterialInventory
()
{
// 物料
List
<
MaterialDef
>
materials
=
Arrays
.
asList
(
new
MaterialDef
(
"FG"
,
"FG"
,
"成品"
,
MaterialDef
.
MaterialType
.
FINISHED
),
new
MaterialDef
(
"SUB"
,
"SUB"
,
"半成品"
,
MaterialDef
.
MaterialType
.
SEMI_FINISHED
),
new
MaterialDef
(
"RAW"
,
"RAW"
,
"原料"
,
MaterialDef
.
MaterialType
.
RAW
));
// 库存策略:RAW 可采购,lotSize=5,最多 10 批/桶
List
<
InventoryPolicy
>
policies
=
Arrays
.
asList
(
new
InventoryPolicy
(
"FG"
,
0
,
0
,
0
,
100
,
0
,
10
),
new
InventoryPolicy
(
"SUB"
,
0
,
0
,
0
,
100
,
0
,
10
),
new
InventoryPolicy
(
"RAW"
,
0
,
0
,
0
,
100
,
0
,
5
,
10
,
1L
));
// BOM:1 FG = 2 SUB(在 FG_O0 开工消耗);1 SUB = 1 RAW(在 SUB_O0 开工消耗)
List
<
BomItemDef
>
bom
=
Arrays
.
asList
(
new
BomItemDef
(
"FG"
,
"SUB"
,
2.0
,
"FG_O0"
,
0
),
new
BomItemDef
(
"SUB"
,
"RAW"
,
1.0
,
"SUB_O0"
,
0
));
// 工序:FG 与 SUB 各一道,均在 M0
List
<
OpDef
>
ops
=
Arrays
.
asList
(
OpDef
.
builder
(
"FG_O0"
,
"成品工序"
).
addResource
(
"M0"
,
10
).
orderId
(
"FG_ORDER"
).
unplannedCost
(
100000
).
build
(),
OpDef
.
builder
(
"SUB_O0"
,
"半成品工序"
).
addResource
(
"M0"
,
10
).
orderId
(
"SUB_ORDER"
).
unplannedCost
(
100000
).
build
());
// 订单:FG 正排生产 10(release=40,给半成品留出生产时间);SUB 倒排生产 20,父订单 FG_ORDER,在 FG_O0 开工前 0 分钟到位
List
<
OrderDef
>
orders
=
Arrays
.
asList
(
new
OrderDef
(
"FG_ORDER"
,
"FG_O0"
,
"FG_O0"
,
40
,
60
,
"FG"
,
10
,
10
,
10
,
10
,
OrderDef
.
PlanDirection
.
FORWARD
,
null
,
null
,
0
),
new
OrderDef
(
"SUB_ORDER"
,
"SUB_O0"
,
"SUB_O0"
,
-
1
,
60
,
"SUB"
,
20
,
20
,
20
,
10
,
OrderDef
.
PlanDirection
.
BACKWARD
,
"FG_ORDER"
,
"FG_O0"
,
0
));
List
<
ResourceDef
>
resources
=
Arrays
.
asList
(
new
ResourceDef
(
"M0"
,
200
));
ModelConfig
cfg
=
new
ModelConfig
();
cfg
.
maxHorizon
=
60
;
// 时变库存时间桶 = 60 分钟
cfg
.
timeLimitSeconds
=
30
;
POAOrToolsModel
scheduler
=
new
POAOrToolsModel
(
ops
,
resources
,
new
ArrayList
<>(),
new
ArrayList
<>(),
new
ArrayList
<>(),
orders
,
cfg
,
materials
,
policies
,
bom
);
scheduler
.
build
();
CpSolver
solver
=
new
CpSolver
();
solver
.
getParameters
().
setMaxTimeInSeconds
(
30
);
CpSolverStatus
status
=
solver
.
solve
(
scheduler
.
getRawModel
());
if
(
status
!=
CpSolverStatus
.
OPTIMAL
&&
status
!=
CpSolverStatus
.
FEASIBLE
)
{
throw
new
AssertionError
(
"时变库存场景求解失败: "
+
status
);
}
long
fgQty
=
solver
.
value
(
scheduler
.
getProductionQtyVars
().
get
(
"FG_ORDER"
));
long
subQty
=
solver
.
value
(
scheduler
.
getProductionQtyVars
().
get
(
"SUB_ORDER"
));
System
.
out
.
printf
(
"[PASS] 时变库存: 状态=%s, FG生产=%d, SUB生产=%d%n"
,
status
,
fgQty
,
subQty
);
// FG 需求 10,SUB 需求 20(10×2)
if
(
fgQty
<
10
*
POAOrToolsModel
.
QUANTITY_SCALE
)
{
throw
new
AssertionError
(
"FG 生产量应 >= 10,实际="
+
fgQty
);
}
if
(
subQty
<
20
*
POAOrToolsModel
.
QUANTITY_SCALE
)
{
throw
new
AssertionError
(
"SUB 生产量应 >= 20,实际="
+
subQty
);
}
}
}
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