Commit b3685a77 authored by Tong Li's avatar Tong Li

Merge remote-tracking branch 'origin/tl'

parents 1e00c75d 391de2d1
......@@ -119,7 +119,7 @@
<dependency>
<groupId>com.google.ortools</groupId>
<artifactId>ortools-java</artifactId>
<version>9.7.2996</version>
<version>9.15.6755</version>
</dependency>
<!-- HTTP客户端 (用于调用LLM API) -->
......
......@@ -56,4 +56,32 @@ public class FileHelper {
System.err.println("Failed to write log: " + e.getMessage());
}
}
public static void writeFile(String message,String fileDir,String fileName) {
String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))+"-";
// 确保目录存在
java.io.File logDir = new java.io.File(fileDir);
if (!logDir.exists()) {
logDir.mkdirs(); // 创建目录(包括父目录)
}
String filePath = fileDir + date + fileName;
try (PrintWriter writer = new PrintWriter(new FileWriter(filePath, true))) {
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
writer.println("[" + timestamp + "] " + message);
System.out.println("[" + timestamp + "] " + message);
} catch (IOException e) {
System.err.println("Failed to write log: " + e.getMessage());
}
}
public static void writeFile(String message,String fileName) {
writeFile(message,LOG_FILE_PATH,fileName);
}
}
\ No newline at end of file
package com.aps.common.util;
import java.io.OutputStream;
import java.io.PrintStream;
/**
* 作者:佟礼
* 时间:2026-07-24
*/
public class TeePrintStream extends PrintStream {
private final PrintStream other;
public TeePrintStream(OutputStream main, PrintStream other) {
super(main);
this.other = other;
}
@Override
public void write(int b) {
super.write(b);
other.write(b);
}
@Override
public void write(byte[] buf, int off, int len) {
super.write(buf, off, len);
other.write(buf, off, len);
}
@Override
public void flush() {
super.flush();
other.flush();
}
@Override
public void close() {
super.close();
other.close();
}
}
package com.aps.macroplanner;
import com.google.ortools.Loader;
import com.aps.macroplanner.data.CoProductTestDataBuilder;
import com.aps.macroplanner.data.TestDataBuilder;
/**
* 联产品/副产品多工序路由测试运行器。
*
* <p>验证场景: 化工反应 2 步工艺路线, 反应工序同时产出主产品 P 和副产品 ByP,
* 验证:
* <ol>
* <li>OP_Reaction 有 2 个产出: P@WIP_R_P_1 + ByP@SP_ByProduct</li>
* <li>RoutingConstraint 确保 PTQty[Reaction] = PTQty[Purify]</li>
* <li>ByP 产量 = PTQty[Reaction], 与主产品 P 同步</li>
* <li>P@SP_FG 物料平衡: 流入 = 流出</li>
* <li>ByP@SP_ByProduct 物料平衡: 联产品流入 = 销售 + 库存</li>
* <li>P@WIP_R_P_1 物料平衡: 流入 = 消耗</li>
* </ol>
*/
public class CoProductTestRunner {
public static void main(String[] args) {
Loader.loadNativeLibraries();
System.out.println("===== 联产品/副产品 多工序路由测试 =====\n");
System.out.println("场景: 化工反应 2 步工艺路线");
System.out.println(" 工序1: OP_Reaction(反应) → 产出 P@WIP + ByP@SP_ByProduct(联产品)");
System.out.println(" 工序2: OP_Purify(提纯) → 消耗 P@WIP, 产出 P@SP_FG");
System.out.println(" 销售: P@SP_FG=50/天, ByP@SP_ByProduct=30/天\n");
TestDataBuilder data = new CoProductTestDataBuilder();
System.out.println("数据加载: " + data.getProducts().size() + " 产品, "
+ data.getOperations().size() + " 工序, "
+ data.getRoutings().size() + " 工艺路线\n");
MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
optimizer.buildModel();
optimizer.solve();
System.out.println("\n===== 联产品/副产品 多工序路由测试 结束 =====");
}
}
\ No newline at end of file
package com.aps.macroplanner;
import com.aps.ApsApplication;
import com.aps.macroplanner.data.DataValidator;
import com.aps.macroplanner.data.MacroPlannerDataConverter;
import com.aps.macroplanner.data.TestDataBuilder;
import com.google.ortools.Loader;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;
/**
* 数据库工艺物料类 → macroplanner 转换器测试运行器。
*
* <p>通过 Spring 启动获取 {@link MacroPlannerDataConverter} Bean,
* 从数据库加载指定场景的工艺物料数据并转换为 {@link TestDataBuilder},
* 然后用 {@link DataValidator} 验证 + {@link MacroPlannerOptimizer} 求解。</p>
*
* <h3>验证流程</h3>
* <ol>
* <li>调用 converter.convert(sceneId) 从数据库转换数据</li>
* <li>打印转换后的实体规模 (products/operations/routings/...)</li>
* <li>DataValidator 验证数据完整性 (引用完整性、非负数等)</li>
* <li>MacroPlannerOptimizer 构建模型并求解</li>
* </ol>
*
* <p>用法: {@code java MacroPlannerDataConverterRunner <sceneId>}</p>
*/
public class MacroPlannerDataConverterRunner {
public static void main(String[] args) {
String sceneId = (args.length > 0) ? args[0] : "B288477F1A594DB584C87EEA77880AA3";
System.out.println("===== MACROPLANNER DATA CONVERTER RUNNER START =====");
System.out.println("SceneId: " + sceneId);
ApplicationContext ctx = SpringApplication.run(ApsApplication.class, args);
try {
MacroPlannerDataConverter converter = ctx.getBean(MacroPlannerDataConverter.class);
// 1. 转换数据: 数据库工艺物料类 → macroplanner 实体
TestDataBuilder data = converter.convert(sceneId);
// 2. 打印转换结果规模
System.out.println("Data converted:");
System.out.println(" Products: " + data.getProducts().size());
System.out.println(" StockingPoints: " + data.getStockingPoints().size());
System.out.println(" Operations: " + data.getOperations().size());
System.out.println(" Routings: " + data.getRoutings().size());
System.out.println(" OperationInputs: " + data.getOperationInputs().size());
System.out.println(" InitialInventories: " + data.getInitialInventories().size());
System.out.println(" InTransitSupplies: " + data.getInTransitSupplies().size());
System.out.println(" SalesDemands: " + data.getSalesDemands().size());
System.out.println(" Periods: " + data.getPeriods().size());
System.out.println(" UnitPeriods: " + data.getUnitPeriods().size());
// 3. DataValidator 验证数据完整性
DataValidator validator = new DataValidator(data);
boolean valid = validator.validate();
if (validator.hasErrors()) {
System.err.println("DataValidator ERRORS (" + validator.getErrors().size() + "):");
validator.getErrors().forEach(e -> System.err.println(" [ERROR] " + e));
}
if (validator.hasWarnings()) {
System.out.println("DataValidator WARNINGS (" + validator.getWarnings().size() + "):");
validator.getWarnings().forEach(w -> System.out.println(" [WARN] " + w));
}
System.out.println("DataValidator: valid=" + valid);
if (!valid) {
System.err.println("数据验证失败, 跳过求解。请检查上述 ERROR。");
return;
}
// 4. 喂给优化器求解
Loader.loadNativeLibraries();
MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
optimizer.buildModel();
optimizer.solve();
System.out.println("===== MACROPLANNER DATA CONVERTER RUNNER END =====");
} finally {
SpringApplication.exit(ctx);
}
}
}
This diff is collapsed.
package com.aps.macroplanner;
import com.google.ortools.Loader;
import com.aps.macroplanner.data.MultiLevelBomTestDataBuilder;
import com.aps.macroplanner.data.TestDataBuilder;
/**
* 多级BOM + 多成品 + 共享半成品 测试运行器。
*
* <p>验证场景:
* <pre>
* P1 ──消耗──→ S1×2.0 + R1×3.0
* P2 ──消耗──→ S1×1.0
* S1 ──消耗──→ R2×2.0
* </pre>
*
* <p>关键验证:
* <ol>
* <li>多成品 P1(40/天) + P2(30/天) 需求同时满足</li>
* <li>共享半成品 S1 总产量 = P1×2.0 + P2×1.0</li>
* <li>多级 BOM 展开: R2 消耗 = S1×2.0, R1 消耗 = P1×3.0</li>
* <li>需求缺口 = 0</li>
* </ol>
*/
public class MultiLevelBomTestRunner {
public static void main(String[] args) {
Loader.loadNativeLibraries();
System.out.println("===== MULTI-LEVEL BOM TEST RUNNER START =====");
System.out.println("BOM: P1→S1×2+R1×3, P2→S1×1, S1→R2×2");
System.out.println("Demand: P1=40/day, P2=30/day");
System.out.println();
TestDataBuilder data = new MultiLevelBomTestDataBuilder();
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("===== MULTI-LEVEL BOM TEST RUNNER END =====");
}
}
\ No newline at end of file
package com.aps.macroplanner;
import com.google.ortools.Loader;
import com.aps.macroplanner.data.RoutingTestDataBuilder;
import com.aps.macroplanner.data.TestDataBuilder;
/**
* 多工序路由测试运行器
*
* <p>验证场景: 3 工序串行生产同一产品, 验证:
* <ol>
* <li>最终产出 = 销售需求 (不是 3×)</li>
* <li>中间 WIP 库存不异常累积</li>
* <li>各工序独立消耗各自设备产能</li>
* </ol>
*/
public class RoutingTestRunner {
public static void main(String[] args) {
Loader.loadNativeLibraries();
System.out.println("===== ROUTING TEST RUNNER START =====");
System.out.println("Scenario: 3-step routing OP_Cut -> OP_Rough -> OP_Finish -> Product P");
System.out.println();
TestDataBuilder data = new RoutingTestDataBuilder();
System.out.println("Data loaded: " + data.getProducts().size() + " products, "
+ data.getOperations().size() + " operations");
MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
optimizer.buildModel();
optimizer.solve();
System.out.println("===== ROUTING TEST RUNNER END =====");
}
}
\ No newline at end of file
package com.aps.macroplanner.constraint;
import com.google.ortools.linearsolver.MPConstraint;
import com.google.ortools.linearsolver.MPSolver;
import com.google.ortools.linearsolver.MPVariable;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
import java.util.Map;
/**
* 需求缺口联动约束 (DemandSlack Linkage)
*
* <p>强制将 SalesDemandQty 与 DemandSlack 关联起来,确保未满足的销售需求
* 必须通过 DemandSlack 变量暴露,从而触发目标函数中的惩罚。</p>
*
* <h3>问题背景</h3>
* <p>在没有此约束时,求解器可以通过降低 SalesDemandQty 来"规避"需求缺口,
* 而不是通过 DemandSlack 来"暴露"需求缺口。因为 DemandSlack 在物料平衡约束中
* 位于流入侧,SalesDemandQty 在流出侧,求解器可以同时降低两者来平衡方程,
* 从而避免触发 DemandSlack 的高额惩罚(权重 100)。</p>
*
* <h3>数学公式</h3>
* <pre>
* 对于每个 PISPIP (Product × StockingPoint × Period):
* Σ SalesDemandQty + DemandSlack >= Σ DemandQuantity
*
* 即: 实际销售量 + 需求缺口 >= 原始需求总量
* 等价于: DemandSlack >= Σ(DemandQuantity - SalesDemandQty)
* </pre>
*
* <h3>效果</h3>
* <p>添加此约束后,当 SalesDemandQty < DemandQuantity 时,
* DemandSlack 必须填补缺口,从而在目标函数中产生惩罚。
* 求解器被激励去增加生产来满足需求,而非简单地降低销售量。</p>
*
* @see BalanceConstraint 物料平衡约束(DemandSlack 在流入侧)
* @see DemandVariableBuilder 需求变量定义(SalesDemandQty, DemandSlack)
*/
public class DemandSlackLinkageConstraint {
/**
* 构建需求缺口联动约束。
*
* <p>对每个 PISPIP 创建一条约束:
* Σ SalesDemandQty + DemandSlack >= Σ DemandQuantity</p>
*
* @param model 模型容器(提供 SalesDemandQty 和 DemandSlack 变量)
* @param data 测试数据(提供 SalesDemand 列表)
*/
public static void build(MacroPlannerModel model, TestDataBuilder data) {
Map<String, MPVariable> sdVars = model.getSalesDemandQtyVars();
Map<String, MPVariable> slackVars = model.getDemandSlackVars();
for (Product prod : data.getProducts()) {
for (StockingPoint sp : data.getStockingPointsForProduct(prod.getId())) {
for (Period p : data.getPeriods()) {
String pispipKey = prod.getId() + "_" + sp.getId() + "_" + p.getIndex();
double totalDemandQty = 0.0;
boolean hasDemand = false;
// 汇总该 PISPIP 的所有销售需求
for (SalesDemand sd : data.getSalesDemandsFor(prod, sp, p)) {
totalDemandQty += sd.getQuantity();
hasDemand = true;
}
// 只对存在销售需求的 PISPIP 创建约束
if (!hasDemand) continue;
// 约束: Σ SalesDemandQty + DemandSlack >= totalDemandQty
MPConstraint c = model.getSolver().makeConstraint(
totalDemandQty, MPSolver.infinity(),
"DSLink_" + pispipKey);
// + Σ SalesDemandQty
for (SalesDemand sd : data.getSalesDemandsFor(prod, sp, p)) {
MPVariable sdVar = sdVars.get(sd.getKey());
if (sdVar != null) c.setCoefficient(sdVar, 1.0);
}
// + DemandSlack
MPVariable slackVar = slackVars.get(pispipKey);
if (slackVar != null) c.setCoefficient(slackVar, 1.0);
}
}
}
}
}
\ No newline at end of file
package com.aps.macroplanner.constraint;
import com.google.ortools.linearsolver.MPConstraint;
import com.google.ortools.linearsolver.MPVariable;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
import java.util.List;
import java.util.Map;
/**
* 工序产量一致性约束 (RoutingConstraint)
*
* <p>对于工艺路线中的每对相邻工序,强制每周期产量相等:
* <pre>
* PTQty[op_i][t] = PTQty[op_{i+1}][t] ∀ 相邻工序对, ∀ 周期 t
* </pre>
*
* <h3>设计动机</h3>
* 在多工序工艺路线 (Routing) 中,物料按序流经各道工序。
* 如果仅靠 BOM 约束 (消耗 ≤ 产出),求解器可能让上游工序
* 过量生产,导致中间 WIP 库存异常堆积,产生难以解释的结果。
*
* <p>此约束强制所有工序保持相同节拍,确保:
* <ul>
* <li>工序间 WIP 不异常累积</li>
* <li>排产结果直观可解释</li>
* <li>各工序产量一致,反映真实工艺路线约束</li>
* </ul>
*
* <h3>对应 Quintiq 模型</h3>
* Quintiq 中 RoutingStep 之间通过 PISPNodeInRouting 流转,
* 配合 LeadTime 自然形成工序间产量一致性。
*
* <h3>约束位置</h3>
* 在 BOM 约束之后、产能约束之前构建,确保工序间产量一致
* 后再施加产能限制。
*/
public class RoutingConstraint {
/**
* 构建工序产量一致性约束。
*
* @param model 模型容器 (提供 PTQty 变量)
* @param data 测试数据 (提供 Routing 列表)
*/
public static void build(MacroPlannerModel model, TestDataBuilder data) {
List<Routing> routings = data.getRoutings();
if (routings.isEmpty()) {
return; // 无工艺路线, 无需构建
}
Map<String, MPVariable> ptQtyVars = model.getPtQtyVars();
List<Period> periods = data.getPeriods();
for (Routing routing : routings) {
List<Operation> ops = routing.getOperations();
if (ops.size() < 2) {
continue; // 单步路由无需工序间约束
}
// 对每对相邻工序 (i, i+1), 强制每周期产量相等
// Σ PTQty[opCurrent][allUnits][t] - Σ PTQty[opNext][allUnits][t] = 0
for (int i = 0; i < ops.size() - 1; i++) {
Operation opCurrent = ops.get(i);
Operation opNext = ops.get(i + 1);
for (Period p : periods) {
MPConstraint con = model.getSolver().makeConstraint(0.0, 0.0,
"RoutingThru_" + routing.getId() + "_" + i + "_" + p.getIndex());
for (UnitOperation uoCur : opCurrent.getUnitOperations()) {
MPVariable ptCur = ptQtyVars.get(opCurrent.ptQtyKey(uoCur, p.getIndex()));
if (ptCur != null) con.setCoefficient(ptCur, 1.0);
}
for (UnitOperation uoNext : opNext.getUnitOperations()) {
MPVariable ptNext = ptQtyVars.get(opNext.ptQtyKey(uoNext, p.getIndex()));
if (ptNext != null) con.setCoefficient(ptNext, -1.0);
}
}
}
}
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Collections;
/**
* 联产品/副产品多工序路由测试数据构建器。
*
* <h3>测试场景: 化工反应工序同时产出主产品 P 和副产品 ByP</h3>
* <pre>
* 原材料 RM ─→ OP_Reaction(反应) ─→ P@WIP_R_P_1 ─→ OP_Purify(提纯) ─→ P@SP_FG(成品)
* │ 消耗 RM@SP_RM×2.0
* │
* └─→ ByP@SP_ByProduct (副产品, 联产品产出)
*
* 工艺路线 R_P (P主生产工艺) 生产产品 P, 最终入库 SP_FG:
* 工序1: OP_Reaction(反应) → Unit_Reactor → 消耗 RM@SP_RM×2.0
* └ 联产品产出: ByP@SP_ByProduct (预配置, expand() 保留)
* └ WIP 产出: P@WIP_R_P_1 (expand() 自动生成)
* 工序2: OP_Purify(提纯) → Unit_Purify → 消耗 P@WIP_R_P_1, 产出 P@SP_FG
* </pre>
*
* <h3>关键验证点</h3>
* <ol>
* <li>OP_Reaction 有 2 个产出: P@WIP_R_P_1 (路由自动) + ByP@SP_ByProduct (联产品)</li>
* <li>RoutingConstraint 确保 PTQty[Reaction] = PTQty[Purify]</li>
* <li>ByP 的产出量 = PTQty[Reaction], 与主产品 P 同步</li>
* <li>P@SP_FG 物料平衡: 流入 (OP_Purify) = 流出 (Sales)</li>
* <li>ByP@SP_ByProduct 物料平衡: 流入 (OP_Reaction 联产品) = 流出 (Sales) + 期末库存</li>
* <li>P@WIP_R_P_1 物料平衡: 流入 (OP_Reaction) = 流出 (OP_Purify 消耗)</li>
* </ol>
*/
public class CoProductTestDataBuilder extends TestDataBuilder {
// 保存关键对象引用, 用于验证
private Product prodP, prodByP, prodRM;
private StockingPoint spFG, spByProduct, spRM;
private Operation opReaction, opPurify, opProcureRM;
private Routing routingP;
public CoProductTestDataBuilder() {
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));
// === 产品: 主产品 P + 副产品 ByP + 原材料 RM ===
prodP = new Product("P", "主产品");
prodByP = new Product("ByP", "副产品");
prodRM = new Product("RM", "原材料");
products.addAll(Arrays.asList(prodP, prodByP, prodRM));
// === 库存点: 成品库 + 副产品库 + 原材料库 ===
spFG = new StockingPoint("SP_FG", "成品库");
spByProduct = new StockingPoint("SP_ByProduct", "副产品库");
spRM = new StockingPoint("SP_RM", "原材料库");
stockingPoints.addAll(Arrays.asList(spFG, spByProduct, spRM));
// === 产品→库存点映射 ===
productSpMappings.add(new ProductSpMapping(prodP, spFG));
productSpMappings.add(new ProductSpMapping(prodByP, spByProduct));
productSpMappings.add(new ProductSpMapping(prodRM, spRM));
// === 关键: 预配置联产品产出 (在 Routing.expand() 之前) ===
// OP_Reaction 除了产出 P@WIP (由路由自动生成), 还产出 ByP@SP_ByProduct (联产品)
opReaction = new Operation("OP_Reaction", "反应", "Unit_Reactor",
1.0, 1.0, false, 0, 1.0); // 无产出构造器
opReaction.addOutput(new OperationOutput(prodByP, spByProduct));
// 此时 opReaction 已有 1 个产出: ByP@SP_ByProduct
// OP_Purify 消耗前道 WIP, 产出成品 P@SP_FG
opPurify = new Operation("OP_Purify", "提纯", "Unit_Purify",
1.0, 1.0, false, 0, 1.0); // 无产出构造器
// === 工艺路线: P 主产品 2 步生产 ===
routingP = new Routing("R_P", "P主生产工艺", prodP, spFG);
routingP.addOperation(opReaction);
routingP.addOperation(opPurify);
operations.addAll(routingP.getOperations());
routings.add(routingP);
// === 展开工艺路线 ===
// expand() 使用 addOutput() 追加产出, 不会覆盖预配置的 ByP@SP_ByProduct
// 展开后 opReaction 有 2 个产出: ByP@SP_ByProduct(联产品) + P@WIP_R_P_1(路由)
// opPurify 有 1 个产出: P@SP_FG(路由)
RoutingExpansion expansion = routingP.expand();
stockingPoints.addAll(expansion.getStockingPoints());
productSpMappings.addAll(expansion.getProductSpMappings());
initialInventories.addAll(expansion.getInitialInventories());
operationInputs.addAll(expansion.getOperationInputs());
// === 投料: OP_Reaction 消耗 RM@SP_RM (每件消耗 2 件原材料) ===
operationInputs.add(new OperationInput(
routingP.getOperations().get(0), prodRM, spRM, 2.0));
// === 原材料采购工序 ===
opProcureRM = new Operation("OP_Procure_RM", "采购原材料", "Unit_RM",
new OperationOutput(prodRM, spRM), 0.5, 1.0, false, 0, 1.0);
operations.add(opProcureRM);
// === 设备产能 ===
for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_Reactor", p, 0.0, 100.0, false));
}
for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_Purify", p, 0.0, 100.0, false));
}
for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_RM", p, 0.0, 200.0, false));
}
// === 初始库存 (全部为 0) ===
initialInventories.add(new InitialInventory(prodP, spFG, 0.0));
initialInventories.add(new InitialInventory(prodByP, spByProduct, 0.0));
initialInventories.add(new InitialInventory(prodRM, spRM, 50.0)); // 给一些初始库存
// === 销售需求 ===
// 主产品 P: 50/周期
for (Period p : periods) {
salesDemands.add(new SalesDemand(prodP, spFG, p, 50.0, 1.0));
}
// 副产品 ByP: 30/周期 (少于 P 的产量, 会有剩余库存)
for (Period p : periods) {
salesDemands.add(new SalesDemand(prodByP, spByProduct, p, 30.0, 1.0));
}
// === 库存规格 ===
for (Period p : periods) {
inventorySpecs.add(new InventorySpec(prodP, spFG, p,
100.0, 10.0, 200.0, true, true, true));
}
for (Period p : periods) {
inventorySpecs.add(new InventorySpec(prodByP, spByProduct, p,
80.0, 10.0, 200.0, true, true, true));
}
for (Period p : periods) {
inventorySpecs.add(new InventorySpec(prodRM, spRM, p,
80.0, 10.0, 500.0, true, true, true));
}
// === 供应规格 ===
// 主产品 P 由提纯工序产出
supplySpecs.add(new SupplySpec("Supply-P", 150.0, 100.0, 300.0,
true, Collections.singletonList(routingP.getOperations().get(1))));
// 副产品 ByP 由反应工序联产品产出
supplySpecs.add(new SupplySpec("Supply-ByP", 150.0, 100.0, 300.0,
true, Collections.singletonList(routingP.getOperations().get(0))));
// 原材料 RM 采购
supplySpecs.add(new SupplySpec("Supply-RM", 200.0, 100.0, 500.0,
true, Collections.singletonList(opProcureRM)));
// === KPI 权重 ===
kpiWeights = new KPIWeights(
100.0, // fulfillmentWeight
10.0, // lotSizeWeight
5.0, // maxInventoryLevelWeight
5.0, // minInventoryLevelWeight
8.0, // targetInventoryLevelWeight
20.0, // unitCapacityWeight
8.0, // supplyTargetWeight
5.0, // minSupplyWeight
5.0, // maxSupplyWeight
1.0, // salesDemandPriorityWeight
20.0, // postponementPenaltyWeight
5.0 // processMaxQuantityWeight
);
}
// ==================== 便捷访问方法 (用于验证) ====================
public Product getProdP() { return prodP; }
public Product getProdByP() { return prodByP; }
public Product getProdRM() { return prodRM; }
public StockingPoint getSpFG() { return spFG; }
public StockingPoint getSpByProduct() { return spByProduct; }
public StockingPoint getSpRM() { return spRM; }
public Operation getOpReaction() { return opReaction; }
public Operation getOpPurify() { return opPurify; }
public Routing getRoutingP() { return routingP; }
}
\ No newline at end of file
This diff is collapsed.
package com.aps.macroplanner.data;
import java.time.LocalDate;
/**
* 在途供应 (InTransitSupply) — 原材料供应商已发货、将在未来到货的固定供应量。
*
* <p>与初始库存不同,在途供应是计划在未来某个日期到达的固定流入量,
* 不是决策变量,而是作为物料平衡约束中的已知常量。</p>
*
* <p>典型场景: 供应商已承诺在 2026-01-07 交付 100 件原材料 R1</p>
*
* <p>arrivalDate 通过 TestDataBuilder.getPeriodByDate() 映射到对应的周期。</p>
*/
public class InTransitSupply {
private final Product product;
private final StockingPoint stockingPoint;
private final LocalDate arrivalDate;
private final double quantity;
public InTransitSupply(Product product, StockingPoint stockingPoint,
LocalDate arrivalDate, double quantity) {
this.product = product;
this.stockingPoint = stockingPoint;
this.arrivalDate = arrivalDate;
this.quantity = quantity;
}
public Product getProduct() { return product; }
public StockingPoint getStockingPoint() { return stockingPoint; }
public LocalDate getArrivalDate() { return arrivalDate; }
public double getQuantity() { return quantity; }
@Override
public String toString() {
return product.getId() + "@" + stockingPoint.getId()
+ " 到货" + arrivalDate + " " + quantity + "件";
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
/**
* 初始库存 — 定义某个产品在某个库存点的期初库存量。
*
* <p>替代之前用字符串 key ("productId_spId") 的 Map 方式, 更清晰。</p>
*/
public class InitialInventory {
private final Product product;
private final StockingPoint stockingPoint;
private final double quantity;
public InitialInventory(Product product, StockingPoint stockingPoint, double quantity) {
this.product = product;
this.stockingPoint = stockingPoint;
this.quantity = quantity;
}
public Product getProduct() { return product; }
public StockingPoint getStockingPoint() { return stockingPoint; }
public double getQuantity() { return quantity; }
@Override
public String toString() {
return product.getId() + "@" + stockingPoint.getId() + " = " + quantity;
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Collections;
/**
* 多级 BOM + 多成品 + 共享半成品 测试数据构建器。
*
* <h3>BOM 结构</h3>
* <pre>
* P1 ──消耗──→ S1 × 2.0 + R1 × 3.0
* P2 ──消耗──→ S1 × 1.0
* S1 ──消耗──→ R2 × 2.0
*
* 物料分级:
* 成品 (Finished): P1, P2
* 半成品 (Semi): S1 (被 P1 和 P2 共享)
* 原材料 (Raw): R1 (P1直接消耗), R2 (S1消耗)
* </pre>
*
* <h3>推导需求 (每周期)</h3>
* <pre>
* P1 销售: 40 → S1需求: 40×2=80, R1需求: 40×3=120
* P2 销售: 30 → S1需求: 30×1=30
* S1 总需求: 80+30=110 → R2需求: 110×2=220
* </pre>
*
* <h3>关键验证点</h3>
* <pre>
* - 共享半成品 S1 的总生产量 = P1生产量×2.0 + P2生产量×1.0
* - 原材料 R2 消耗 = S1生产量×2.0
* - 原材料 R1 消耗 = P1生产量×3.0
* - 所有需求满足 (需求缺口=0)
* - 多级 BOM 展开正确
* </pre>
*/
public class MultiLevelBomTestDataBuilder extends TestDataBuilder {
@Override
protected void build() {
// === 周期: 3 天 ===
periods.add(new Period(0, "Day1", LocalDate.of(2026, 8, 7)));
periods.add(new Period(1, "Day2", LocalDate.of(2026, 8, 8)));
periods.add(new Period(2, "Day3", LocalDate.of(2026, 8, 9)));
// === 产品: 成品 P1, P2 + 半成品 S1 + 原材料 R1, R2 ===
Product prodP1 = new Product("P1", "成品P1");
Product prodP2 = new Product("P2", "成品P2");
Product prodS1 = new Product("S1", "半成品S1");
Product prodR1 = new Product("R1", "原材料R1");
Product prodR2 = new Product("R2", "原材料R2");
products.addAll(Arrays.asList(prodP1, prodP2, prodS1, prodR1, prodR2));
// === 库存点: 每种产品一个库存点 ===
StockingPoint spP1 = new StockingPoint("SP_P1", "P1成品库");
StockingPoint spP2 = new StockingPoint("SP_P2", "P2成品库");
StockingPoint spSemi = new StockingPoint("SP_Semi", "半成品库");
StockingPoint spR1 = new StockingPoint("SP_R1", "R1原材料库");
StockingPoint spR2 = new StockingPoint("SP_R2", "R2原材料库");
stockingPoints.addAll(Arrays.asList(spP1, spP2, spSemi, spR1, spR2));
// === 产品→库存点映射 ===
productSpMappings.add(new ProductSpMapping(prodP1, spP1));
productSpMappings.add(new ProductSpMapping(prodP2, spP2));
productSpMappings.add(new ProductSpMapping(prodS1, spSemi));
productSpMappings.add(new ProductSpMapping(prodR1, spR1));
productSpMappings.add(new ProductSpMapping(prodR2, spR2));
// === 生产工序: 成品 + 半成品 ===
// P1: 消耗 S1×2.0 + R1×3.0, 产出 P1@SP_P1
Operation opP1 = new Operation("OP_P1", "生产P1", "Unit_P1",
new OperationOutput(prodP1, spP1), 1.0, 1.0, false, 0, 1.0);
// P2: 消耗 S1×1.0, 产出 P2@SP_P2
Operation opP2 = new Operation("OP_P2", "生产P2", "Unit_P2",
new OperationOutput(prodP2, spP2), 1.0, 1.0, false, 0, 1.0);
// S1: 消耗 R2×2.0, 产出 S1@SP_Semi
Operation opS1 = new Operation("OP_S1", "生产S1", "Unit_S1",
new OperationOutput(prodS1, spSemi), 1.0, 1.0, false, 0, 1.0);
operations.addAll(Arrays.asList(opP1, opP2, opS1));
// === 原材料采购工序 ===
Operation opProcureR1 = new Operation("OP_Procure_R1", "采购R1", "Unit_R1",
new OperationOutput(prodR1, spR1), 0.5, 1.0, false, 0, 1.0);
Operation opProcureR2 = new Operation("OP_Procure_R2", "采购R2", "Unit_R2",
new OperationOutput(prodR2, spR2), 0.5, 1.0, false, 0, 1.0);
operations.addAll(Arrays.asList(opProcureR1, opProcureR2));
// === BOM 投料: P1 消耗 S1×2.0 + R1×3.0 ===
operationInputs.add(new OperationInput(opP1, prodS1, spSemi, 2.0));
operationInputs.add(new OperationInput(opP1, prodR1, spR1, 3.0));
// === BOM 投料: P2 消耗 S1×1.0 ===
operationInputs.add(new OperationInput(opP2, prodS1, spSemi, 1.0));
// === BOM 投料: S1 消耗 R2×2.0 ===
operationInputs.add(new OperationInput(opS1, prodR2, spR2, 2.0));
// === 设备产能: 每个设备 200h/周期 ===
for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_P1", p, 0.0, 200.0, false));
unitPeriods.add(new UnitPeriod("Unit_P2", p, 0.0, 200.0, false));
unitPeriods.add(new UnitPeriod("Unit_S1", p, 0.0, 200.0, false));
unitPeriods.add(new UnitPeriod("Unit_R1", p, 0.0, 200.0, false));
unitPeriods.add(new UnitPeriod("Unit_R2", p, 0.0, 200.0, false));
}
// === 初始库存: 全部从 0 开始 ===
initialInventories.add(new InitialInventory(prodP1, spP1, 0.0));
initialInventories.add(new InitialInventory(prodP2, spP2, 0.0));
initialInventories.add(new InitialInventory(prodS1, spSemi, 0.0));
initialInventories.add(new InitialInventory(prodR1, spR1, 0.0));
initialInventories.add(new InitialInventory(prodR2, spR2, 0.0));
// === 销售需求: P1=40/周期, P2=30/周期 ===
for (Period p : periods) {
salesDemands.add(new SalesDemand(prodP1, spP1, p, 40.0, 1.0));
salesDemands.add(new SalesDemand(prodP2, spP2, p, 30.0, 1.0));
}
// === 库存规格: 目标库存 ===
for (Period p : periods) {
inventorySpecs.add(new InventorySpec(prodP1, spP1, p,
80.0, 10.0, 200.0, true, true, true));
inventorySpecs.add(new InventorySpec(prodP2, spP2, p,
60.0, 10.0, 200.0, true, true, true));
inventorySpecs.add(new InventorySpec(prodS1, spSemi, p,
100.0, 10.0, 300.0, true, true, true));
inventorySpecs.add(new InventorySpec(prodR1, spR1, p,
80.0, 10.0, 500.0, true, true, true));
inventorySpecs.add(new InventorySpec(prodR2, spR2, p,
80.0, 10.0, 500.0, true, true, true));
}
// === 供应规格: 成品+半成品各一个, 原材料各一个 ===
supplySpecs.add(new SupplySpec("Supply-P1", 120.0, 80.0, 300.0,
true, Collections.singletonList(opP1)));
supplySpecs.add(new SupplySpec("Supply-P2", 90.0, 60.0, 300.0,
true, Collections.singletonList(opP2)));
supplySpecs.add(new SupplySpec("Supply-S1", 300.0, 200.0, 500.0,
true, Collections.singletonList(opS1)));
supplySpecs.add(new SupplySpec("Supply-R1", 400.0, 200.0, 800.0,
true, Collections.singletonList(opProcureR1)));
supplySpecs.add(new SupplySpec("Supply-R2", 600.0, 400.0, 1000.0,
true, Collections.singletonList(opProcureR2)));
// === KPI 权重 (必须显式初始化, 因为覆写了 build() 不调用 super) ===
kpiWeights = new KPIWeights(
100.0, // fulfillmentWeight
10.0, // lotSizeWeight
5.0, // maxInventoryLevelWeight
5.0, // minInventoryLevelWeight
8.0, // targetInventoryLevelWeight
20.0, // unitCapacityWeight
8.0, // supplyTargetWeight
5.0, // minSupplyWeight
5.0, // maxSupplyWeight
1.0, // salesDemandPriorityWeight
20.0, // postponementPenaltyWeight
5.0 // processMaxQuantityWeight
);
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
/**
* 操作输出 (OperationOutput) — 对应 Quintiq 中的 OperationOutput → PISP
*
* <p>定义某个操作 (Operation) 生产的产品及其产出到的库存点。
* 与 {@link OperationInput} 对称: 输入用 Product + StockingPoint, 输出也用 Product + StockingPoint。</p>
*
* <h3>多工序路由场景</h3>
* <pre>
* 下料 → OperationOutput(P, SP_WIP1)
* 粗加工 → OperationOutput(P, SP_WIP2)
* 精加工 → OperationOutput(P, SP_FG)
* </pre>
* 同一产品经不同工序产出到不同库存点, 通过 {@link OperationOutput} 的 stockingPoint 区分。
*/
public class OperationOutput {
private final Product product; // 产出产品
private final StockingPoint stockingPoint; // 产出到哪个库存点
public OperationOutput(Product product, StockingPoint stockingPoint) {
this.product = product;
this.stockingPoint = stockingPoint;
}
public Product getProduct() { return product; }
public StockingPoint getStockingPoint() { return stockingPoint; }
/** 便捷方法: 产品 ID */
public String getProductId() { return product.getId(); }
/** 便捷方法: 库存点 ID */
public String getSpId() { return stockingPoint.getId(); }
@Override
public String toString() {
return product.getId() + "@" + stockingPoint.getId();
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
/**
* 产品→库存点映射 — 定义一个产品存放在哪个库存点。
*
* <p>替代之前用 Map<String, List<StockingPoint>> 的 productSpMap 方式,
* 声明式地定义产品与库存点的关系。</p>
*
* <p>一个产品可以在多个库存点存放 (通过多个 ProductSpMapping 记录)。</p>
*/
public class ProductSpMapping {
private final Product product;
private final StockingPoint stockingPoint;
public ProductSpMapping(Product product, StockingPoint stockingPoint) {
this.product = product;
this.stockingPoint = stockingPoint;
}
public Product getProduct() { return product; }
public StockingPoint getStockingPoint() { return stockingPoint; }
@Override
public String toString() {
return product.getId() + " → " + stockingPoint.getId();
}
}
\ No newline at end of file
This diff is collapsed.
package com.aps.macroplanner.data;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 工艺路线展开结果 — 包含 {@link Routing#expand} 自动生成的所有数据。
*
* <p>调用方将展开结果中的各项数据合并到 {@link TestDataBuilder} 中。</p>
*
* <h3>展开内容</h3>
* <pre>
* - stockingPoints: 中间 WIP 库存点 (如 WIP_R001_1, WIP_R001_2)
* - productSpMappings: 产品→WIP 库存点映射
* - initialInventories: 中间 WIP 初始库存 (均为 0)
* - operationInputs: 工序间 BOM 消耗关系
* </pre>
*
* @see Routing
*/
public class RoutingExpansion {
private final Routing routing;
/** 自动生成的中间 WIP 库存点 */
private final List<StockingPoint> stockingPoints = new ArrayList<>();
/** 产品→WIP 库存点映射 */
private final List<ProductSpMapping> productSpMappings = new ArrayList<>();
/** 中间 WIP 初始库存 (均为 0) */
private final List<InitialInventory> initialInventories = new ArrayList<>();
/** 工序间 BOM 消耗关系 */
private final List<OperationInput> operationInputs = new ArrayList<>();
RoutingExpansion(Routing routing) {
this.routing = routing;
}
void addStockingPoint(StockingPoint sp) {
stockingPoints.add(sp);
}
void addProductSpMapping(ProductSpMapping mapping) {
productSpMappings.add(mapping);
}
void addInitialInventory(InitialInventory inv) {
initialInventories.add(inv);
}
void addOperationInput(OperationInput input) {
operationInputs.add(input);
}
// ==================== Getters ====================
public Routing getRouting() { return routing; }
public List<StockingPoint> getStockingPoints() { return Collections.unmodifiableList(stockingPoints); }
public List<ProductSpMapping> getProductSpMappings() { return Collections.unmodifiableList(productSpMappings); }
public List<InitialInventory> getInitialInventories() { return Collections.unmodifiableList(initialInventories); }
public List<OperationInput> getOperationInputs() { return Collections.unmodifiableList(operationInputs); }
}
\ No newline at end of file
package com.aps.macroplanner.data;
/**
* 工艺路线中的一个工序步骤 — 对应 Quintiq 中的 RoutingStep。
*
* <p>每个 RoutingStep 包含一个 Operation 和它在工艺路线中的顺序号。
* 工序间的 WIP 由 {@link Routing} 自动管理, 无需手动配置库存点。</p>
*
* <h3>与 Quintiq 模型的对应关系</h3>
* <pre>
* Quintiq RoutingStep:
* - SequenceNumber → sequenceNumber
* - Operation → operation
* - RoutingID → 由父 Routing 管理
* - PISPNodeInRouting → 由 Routing.expand() 自动生成 WIP 库存点
* </pre>
*
* @see Routing
*/
public class RoutingStep {
/** 工序顺序号 (从 1 开始, 越小越靠前) */
private final int sequenceNumber;
/** 该步骤执行的工序 */
private final Operation operation;
/**
* 创建工艺路线步骤。
*
* @param sequenceNumber 顺序号 (1-based, 越小越靠前)
* @param operation 该步骤执行的工序
*/
public RoutingStep(int sequenceNumber, Operation operation) {
this.sequenceNumber = sequenceNumber;
this.operation = operation;
}
public int getSequenceNumber() { return sequenceNumber; }
public Operation getOperation() { return operation; }
}
\ No newline at end of file
package com.aps.macroplanner.data;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Collections;
/**
* 多工序路由测试数据构建器 — 使用 {@link Routing} 自动管理工序间 WIP。
*
* <h3>测试场景: 3 工序串行生产同一产品 P, 第一道工序投料消耗原材料 RM</h3>
* <pre>
* 原材料 RM → OP_Cut(下料) → WIP_R001_1 → OP_Rough(粗加工) → WIP_R001_2 → OP_Finish(精加工) → SP_FG(成品)
* ↑消耗RM@SP_RM×1
*
* 工艺路线 R001 (P生产工艺) 生产产品 P, 最终入库 SP_FG:
* 工序1: OP_Cut(下料) → Unit_Cut → 消耗 RM@SP_RM × 1.0 (投料)
* 工序2: OP_Rough(粗加工) → Unit_Rough → 消耗 WIP_R001_1
* 工序3: OP_Finish(精加工) → Unit_Finish → 产出成品到 SP_FG
*
* 工序间 WIP 由 Routing.expand() 自动生成:
* - 自动创建 WIP_R001_1 (工序1→2 缓冲区)
* - 自动创建 WIP_R001_2 (工序2→3 缓冲区)
* - 自动配置 BOM: OP_Rough 消耗 WIP_R001_1, OP_Finish 消耗 WIP_R001_2
* - 只有 OP_Finish 产出成品, 中间 WIP 不重复计算
* </pre>
*
* <h3>关键验证点</h3>
* <pre>
* - 原材料 RM 通过 BOM 被 OP_Cut 消耗, 每生产 1 件 P 消耗 1 件 RM
* - 3 个工序各自消耗各自设备的产能 (独立计算)
* - 只有最后一道工序的产出计入成品供应
* - 中间 WIP 被 BOM 约束完全消耗 + RoutingConstraint 强制产量一致
* - 用户无需手动配置中间 WIP 库存点、BOM 输入
* </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));
// === 产品: 成品 P + 原材料 RM ===
Product prodP = new Product("P", "Product-P");
Product prodRM = new Product("RM", "RawMaterial");
products.addAll(Arrays.asList(prodP, prodRM));
// === 库存点: 成品库 + 原材料库 ===
StockingPoint spFG = new StockingPoint("SP_FG", "成品库");
StockingPoint spRM = new StockingPoint("SP_RM", "原材料库");
stockingPoints.addAll(Arrays.asList(spFG, spRM));
// === 产品→库存点映射 ===
productSpMappings.add(new ProductSpMapping(prodP, spFG));
productSpMappings.add(new ProductSpMapping(prodRM, spRM));
// === 原材料采购工序 (供应 RM 到 SP_RM) ===
Operation opProcureRM = new Operation("OP_Procure_RM", "采购原材料", "Unit_RM",
new OperationOutput(prodRM, spRM), 0.5, 1.0, false, 0, 1.0);
operations.add(opProcureRM);
// === 工艺路线: 产品 P 经过 3 道工序生产, 最终入库 SP_FG ===
// 对应 Quintiq: Routing R001, 关联 Product P 和 StockingPoint SP_FG
Routing routing = new Routing("R001", "P生产工艺", prodP, spFG);
routing.addOperation(new Operation("OP_Cut", "下料", "Unit_Cut", 1.0, 1.0, false, 0, 1.0));
routing.addOperation(new Operation("OP_Rough", "粗加工", "Unit_Rough", 1.5, 1.0, false, 0, 1.0));
routing.addOperation(new Operation("OP_Finish","精加工", "Unit_Finish",2.0, 1.0, false, 0, 1.0));
operations.addAll(routing.getOperations());
routings.add(routing); // 注册到数据构建器, 供 RoutingConstraint 使用
// === 展开工艺路线: 自动生成 WIP 库存点 + BOM 输入 + 工序产出配置 ===
// 只有最后工序 (OP_Finish) 产出成品, 中间工序 WIP 自动流转
RoutingExpansion expansion = routing.expand();
stockingPoints.addAll(expansion.getStockingPoints());
productSpMappings.addAll(expansion.getProductSpMappings());
initialInventories.addAll(expansion.getInitialInventories());
operationInputs.addAll(expansion.getOperationInputs());
// === 投料: OP_Cut 消耗原材料 RM@SP_RM (每件 P 消耗 1 件 RM) ===
operationInputs.add(new OperationInput(
routing.getOperations().get(0), prodRM, spRM, 1.0));
// === 设备产能 ===
for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_Cut", p, 0.0, 200.0, false));
}
for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_Rough", p, 0.0, 200.0, false));
}
for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_Finish", p, 0.0, 200.0, false));
}
for (Period p : periods) {
unitPeriods.add(new UnitPeriod("Unit_RM", p, 0.0, 200.0, false));
}
// === 初始库存 ===
initialInventories.add(new InitialInventory(prodP, spFG, 0.0));
initialInventories.add(new InitialInventory(prodRM, spRM, 0.0));
// === 销售需求: 只在成品库 SP_FG, 每周期 50 ===
for (Period p : periods) {
salesDemands.add(new SalesDemand(prodP, spFG, p, 50.0, 1.0));
}
// === 库存规格 ===
for (Period p : periods) {
inventorySpecs.add(new InventorySpec(prodP, spFG, p,
80.0, 10.0, 200.0, true, true, true));
}
for (Period p : periods) {
inventorySpecs.add(new InventorySpec(prodRM, spRM, p,
80.0, 10.0, 500.0, true, true, true));
}
// === 供应规格: 精加工产出 + 原材料采购 ===
supplySpecs.add(new SupplySpec("Supply-P", 150.0, 100.0, 300.0,
true, Collections.singletonList(routing.getOperations().get(2))));
supplySpecs.add(new SupplySpec("Supply-RM", 200.0, 100.0, 500.0,
true, Collections.singletonList(opProcureRM)));
// === KPI 权重 ===
kpiWeights = new KPIWeights(
100.0, // fulfillmentWeight
10.0, // lotSizeWeight
5.0, // maxInventoryLevelWeight
5.0, // minInventoryLevelWeight
8.0, // targetInventoryLevelWeight
3.0, // unitCapacityWeight
8.0, // supplyTargetWeight
5.0, // minSupplyWeight
5.0, // maxSupplyWeight
1.0, // salesDemandPriorityWeight
20.0, // postponementPenaltyWeight
5.0 // processMaxQuantityWeight
);
}
}
\ No newline at end of file
package com.aps.macroplanner.data;
/**
* 单元-工序关联 — 定义某个工序在某个单元(Unit)上的产能参数。
*
* <p>一个 Operation 可以有多个 UnitOperation,表示同一工序可在不同单元/产线执行,
* 每个单元的产能消耗、批次大小可能不同。</p>
*
* <p>例如: "采购 R1" 可以由供应商 A (0.5h/件) 和供应商 B (0.8h/件) 执行。</p>
*/
public class UnitOperation {
private final String unitId; // 所属单元ID
private final double capacityCoeff; // 产能消耗系数 (单件耗时)
private final boolean hasLotSize; // 是否有批次大小
private final double lotSize; // 批次大小
private final double qtpfactor; // QuantityToProcessFactor
public UnitOperation(String unitId, double capacityCoeff,
boolean hasLotSize, double lotSize, double qtpfactor) {
this.unitId = unitId;
this.capacityCoeff = capacityCoeff;
this.hasLotSize = hasLotSize;
this.lotSize = lotSize;
this.qtpfactor = qtpfactor;
}
public String getUnitId() { return unitId; }
public double getCapacityCoeff() { return capacityCoeff; }
public boolean hasLotSize() { return hasLotSize; }
public double getLotSize() { return lotSize; }
public double getQtpfactor() { return qtpfactor; }
@Override
public String toString() {
return unitId + "(产能" + capacityCoeff + "h/件" +
(hasLotSize ? ", 批次" + lotSize : "") + ")";
}
}
\ No newline at end of file
package com.aps.macroplanner.objective;
import com.google.ortools.linearsolver.MPVariable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 策略层级配置 — 对应 Quintiq 中的 StrategyLevel
*
* <p>每个层级包含一组 KPI,在分层优化中作为一个整体求解。
* 层级越低(level 值越小),优先级越高,越先求解。</p>
*
* <h3>分层优化流程</h3>
* <pre>
* 第1轮: 只优化 Level 1 的 KPI → 记录最优值 V1
* 第2轮: 约束 Level1 ≤ V1×(1+slack) → 优化 Level 2 的 KPI
* 第3轮: 约束 Level1+2 ≤ 最优×(1+slack) → 优化 Level 3 的 KPI
* ...
* </pre>
*
* <h3>默认层级划分</h3>
* <pre>
* Level 1 (需求满足): Fulfillment (需求缺口 = 硬约束)
* Level 2 (产能): UnitCapacity (产能超载 = 物理约束)
* Level 3 (业务KPI): LotSize, TargetInventory, SupplyTarget, SalesPriority
* Level 4 (软约束): MaxInventory, MinInventory, MinSupply, MaxSupply
* </pre>
*
* @see ObjectiveBuilder
* @see com.aps.macroplanner.MacroPlannerOptimizer
*/
public class StrategyLevel {
/** 层级编号 (1 最高优先, 数值越大优先级越低) */
private final int level;
/** 层级名称 (用于日志输出) */
private final String name;
/**
* 目标松弛比例 — 允许上层最优值退化的比例。
* 例如 0.0 表示不允许退化(严格分层),
* 0.05 表示允许上层目标值恶化 5% 以换取下层优化空间。
* 对应 Quintiq 中的 RelativeGoalSlack。
*/
private final double relativeGoalSlack;
/** 该层级包含的 KPI 条目列表 */
private final List<KPIEntry> kpis = new ArrayList<>();
/**
* 单个 KPI 条目 — 封装变量、权重和方向。
*/
public static class KPIEntry {
/** 该 KPI 的汇总变量 (如 TotalFulfillment) */
public final MPVariable variable;
/** 该 KPI 在当前层级内的权重 */
public final double weight;
/** 是否为负向 KPI (越小越好 = 正常惩罚项; false = 正常惩罚项) */
public final boolean isNegative;
/** KPI 名称 (用于日志) */
public final String name;
public KPIEntry(String name, MPVariable variable, double weight, boolean isNegative) {
this.name = name;
this.variable = variable;
this.weight = weight;
this.isNegative = isNegative;
}
/**
* 计算该 KPI 在目标函数中的实际系数。
* 负向 KPI (如 SalesDemandPriority) 使用负系数实现最大化。
*/
public double effectiveCoefficient() {
return isNegative ? -weight : weight;
}
}
/**
* 创建策略层级。
*
* @param level 层级编号 (1-based, 越小优先级越高)
* @param name 层级名称
* @param relativeGoalSlack 目标松弛比例 (0.0 = 严格分层)
*/
public StrategyLevel(int level, String name, double relativeGoalSlack) {
this.level = level;
this.name = name;
this.relativeGoalSlack = relativeGoalSlack;
}
/**
* 添加一个 KPI 到该层级。
*
* @param name KPI 名称
* @param variable KPI 汇总变量
* @param weight 权重 (0 = 跳过)
*/
public void addKPI(String name, MPVariable variable, double weight) {
addKPI(name, variable, weight, false);
}
/**
* 添加一个 KPI 到该层级。
*
* @param name KPI 名称
* @param variable KPI 汇总变量
* @param weight 权重 (0 = 跳过)
* @param isNegative 是否为负向 KPI (true = 最大化, 使用负系数)
*/
public void addKPI(String name, MPVariable variable, double weight, boolean isNegative) {
if (weight > 0.0 && variable != null) {
kpis.add(new KPIEntry(name, variable, weight, isNegative));
}
}
// ==================== Getters ====================
public int getLevel() { return level; }
public String getName() { return name; }
public double getRelativeGoalSlack() { return relativeGoalSlack; }
public List<KPIEntry> getKpis() { return Collections.unmodifiableList(kpis); }
/**
* 该层级是否有任何有效的 KPI。
*/
public boolean hasKpis() {
return !kpis.isEmpty();
}
}
\ No newline at end of file
package com.aps.macroplanner.output;
import java.util.List;
/**
* 轻量级 JSON 字符串构建器 — 无外部依赖, 兼容 Java 8。
*
* <p>提供以下方法:</p>
* <ul>
* <li>{@link #obj()} / {@link #endObj()} — 开始/结束 JSON 对象</li>
* <li>{@link #arr()} / {@link #endArr()} — 开始/结束 JSON 数组</li>
* <li>{@link #key(String)} — 写入键</li>
* <li>{@link #val(String)} / {@link #val(double)} / {@link #val(int)} / {@link #val(boolean)} / {@link #valNull()} — 写入值</li>
* </ul>
*
* <h3>使用示例</h3>
* <pre>{@code
* JsonBuilder jb = new JsonBuilder();
* jb.obj()
* .key("name").val("Alice")
* .key("age").val(30)
* .key("scores").arr()
* .val(95.5).val(88.0)
* .endArr()
* .endObj();
* String json = jb.toString();
* }</pre>
*/
public class JsonBuilder {
private final StringBuilder sb = new StringBuilder();
private boolean firstInContainer = true;
/** 开始 JSON 对象 { */
public JsonBuilder obj() {
sb.append("{");
firstInContainer = true;
return this;
}
/** 结束 JSON 对象 } */
public JsonBuilder endObj() {
sb.append("}");
return this;
}
/** 开始 JSON 数组 [ */
public JsonBuilder arr() {
sb.append("[");
firstInContainer = true;
return this;
}
/** 结束 JSON 数组 ] */
public JsonBuilder endArr() {
sb.append("]");
return this;
}
/** 写入键 "key": */
public JsonBuilder key(String key) {
if (!firstInContainer) sb.append(",");
sb.append("\"").append(escape(key)).append("\":");
firstInContainer = true;
return this;
}
/** 写入字符串值 */
public JsonBuilder val(String value) {
comma();
if (value == null) {
sb.append("null");
} else {
sb.append("\"").append(escape(value)).append("\"");
}
return this;
}
/** 写入 double 值 (有限小数的 JSON 数字) */
public JsonBuilder val(double value) {
comma();
if (Double.isNaN(value) || Double.isInfinite(value)) {
sb.append("null");
} else if (Math.abs(value - Math.round(value)) < 1e-9) {
sb.append((long) value);
} else {
sb.append(String.format("%.6f", value));
}
return this;
}
/** 写入 int 值 */
public JsonBuilder val(int value) {
comma();
sb.append(value);
return this;
}
/** 写入 long 值 */
public JsonBuilder val(long value) {
comma();
sb.append(value);
return this;
}
/** 写入 boolean 值 */
public JsonBuilder val(boolean value) {
comma();
sb.append(value);
return this;
}
/** 写入 null */
public JsonBuilder valNull() {
comma();
sb.append("null");
return this;
}
/**
* 写入可选的 double 值 (null 时输出 null)。
*/
public JsonBuilder valOpt(Double value) {
if (value == null) {
return valNull();
}
return val(value.doubleValue());
}
/**
* 写入 JSON 对象列表 (每个元素调用 toJson 方法)。
*/
public <T extends JsonSerializable> JsonBuilder valArray(List<T> items) {
arr();
for (int i = 0; i < items.size(); i++) {
if (i > 0) sb.append(",");
items.get(i).toJson(this);
}
endArr();
return this;
}
/** 在值前插入逗号 (如果不是第一个) */
private void comma() {
if (!firstInContainer) sb.append(",");
firstInContainer = false;
}
/** JSON 字符串转义 */
private static String escape(String s) {
StringBuilder out = new StringBuilder(s.length() + 8);
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '"': out.append("\\\""); break;
case '\\': out.append("\\\\"); break;
case '\b': out.append("\\b"); break;
case '\f': out.append("\\f"); break;
case '\n': out.append("\\n"); break;
case '\r': out.append("\\r"); break;
case '\t': out.append("\\t"); break;
default:
if (c < 0x20) {
out.append(String.format("\\u%04x", (int) c));
} else {
out.append(c);
}
}
}
return out.toString();
}
@Override
public String toString() {
return sb.toString();
}
// ==================== 序列化接口 ====================
/**
* 可序列化为 JSON 的对象接口。
*/
public interface JsonSerializable {
void toJson(JsonBuilder jb);
}
}
\ No newline at end of file
This diff is collapsed.
package com.aps.macroplanner.output.dto;
import java.util.ArrayList;
import java.util.List;
/**
* 需求满足汇总 — 按产品/周期聚合需求量、满足量、满足率。
*
* <h3>结构</h3>
* <pre>
* productId: 产品ID
* totalDemand: 总需求量
* totalFulfilled: 总满足量
* totalUnmet: 总未满足量
* fulfillmentRate: 整体满足率
* periodEntries: 每周期明细
* </pre>
*/
public class DemandSummaryResult {
private String productId;
/** 按周期明细 */
private final List<PeriodEntry> periodEntries = new ArrayList<>();
/** 跨周期汇总 */
private double totalDemand;
private double totalFulfilled;
private double totalUnmet;
private double fulfillmentRate;
// ==================== 内嵌类 ====================
public static class PeriodEntry {
public int periodIndex;
public String periodStartDate;
public double demandQty;
public double fulfilledQty;
public double unmetQty;
public double fulfillmentRate;
}
// ==================== Getters / Setters ====================
public String getProductId() { return productId; }
public void setProductId(String v) { this.productId = v; }
public List<PeriodEntry> getPeriodEntries() { return periodEntries; }
public double getTotalDemand() { return totalDemand; }
public void setTotalDemand(double v) { this.totalDemand = v; }
public double getTotalFulfilled() { return totalFulfilled; }
public void setTotalFulfilled(double v) { this.totalFulfilled = v; }
public double getTotalUnmet() { return totalUnmet; }
public void setTotalUnmet(double v) { this.totalUnmet = v; }
public double getFulfillmentRate() { return fulfillmentRate; }
public void setFulfillmentRate(double v) { this.fulfillmentRate = v; }
}
package com.aps.macroplanner.output.dto;
import java.util.ArrayList;
import java.util.List;
/**
* KPI 汇总结果 — 对应 Quintiq 中 OptimizerNonFinancialKPIResult 的快照数据。
*/
public class KpiResult {
/** 目标函数值 (加权总惩罚) */
private double objectiveValue;
/** 各项 KPI 明细 */
private final List<KpiEntry> entries = new ArrayList<>();
// ==================== 内嵌类 ====================
/** 单个 KPI 项 */
public static class KpiEntry {
/** KPI 名称 */
public String name;
/** KPI 原始值 (松弛量总和) */
public double rawValue;
/** 权重 */
public double weight;
/** 加权惩罚 = rawValue × weight */
public double penalty;
/** 是否为收益项 (越大越好, 正系数) */
public boolean isBenefit;
}
// ==================== Getters / Setters ====================
public double getObjectiveValue() { return objectiveValue; }
public void setObjectiveValue(double v) { this.objectiveValue = v; }
public List<KpiEntry> getEntries() { return entries; }
public void addEntry(String name, double rawValue, double weight, boolean isBenefit) {
KpiEntry e = new KpiEntry();
e.name = name;
e.rawValue = rawValue;
e.weight = weight;
e.isBenefit = isBenefit;
e.penalty = isBenefit ? -rawValue * weight : rawValue * weight;
entries.add(e);
}
}
\ No newline at end of file
package com.aps.macroplanner.output.dto;
import java.util.ArrayList;
import java.util.List;
/**
* 优化结果顶层容器 — 对应 Quintiq 中一次完整的优化运行输出。
*
* <p>包含以下子结构:</p>
* <ul>
* <li>metadata — 运行时间戳、求解器、版本</li>
* <li>periodTasks — 生产任务结果 (← PTQty)</li>
* <li>salesDemands — 销售需求满足结果 (← SalesDemandQty)</li>
* <li>pispips — 库存点库存结果 (← InvQty, DemandSlack 等)</li>
* <li>kpis — KPI 汇总结果</li>
* <li>statistics — 求解器运行统计</li>
* </ul>
*/
public class OptimizationResult {
// ==================== 元数据 ====================
private String timestamp;
private String solver;
private String version;
// ==================== 业务结果 ====================
private final List<PeriodTaskResult> periodTasks = new ArrayList<>();
private final List<SalesDemandResult> salesDemands = new ArrayList<>();
private final List<PispipResult> pispips = new ArrayList<>();
// ==================== 产品生产网络 ====================
private ProductNetworkResult productNetwork;
// ==================== Unit产能使用 ====================
private List<UnitCapacityResult> unitCapacities;
// ==================== 需求满足汇总 ====================
private List<DemandSummaryResult> demandSummary;
// ==================== KPI 和统计 ====================
private KpiResult kpis;
private SolverStatistics statistics;
// ==================== Getters / Setters ====================
public String getTimestamp() { return timestamp; }
public void setTimestamp(String v) { this.timestamp = v; }
public String getSolver() { return solver; }
public void setSolver(String v) { this.solver = v; }
public String getVersion() { return version; }
public void setVersion(String v) { this.version = v; }
public List<PeriodTaskResult> getPeriodTasks() { return periodTasks; }
public List<SalesDemandResult> getSalesDemands() { return salesDemands; }
public List<PispipResult> getPispips() { return pispips; }
public ProductNetworkResult getProductNetwork() { return productNetwork; }
public void setProductNetwork(ProductNetworkResult v) { this.productNetwork = v; }
public List<UnitCapacityResult> getUnitCapacities() { return unitCapacities; }
public void setUnitCapacities(List<UnitCapacityResult> v) { this.unitCapacities = v; }
public List<DemandSummaryResult> getDemandSummary() { return demandSummary; }
public void setDemandSummary(List<DemandSummaryResult> v) { this.demandSummary = v; }
public KpiResult getKpis() { return kpis; }
public void setKpis(KpiResult v) { this.kpis = v; }
public SolverStatistics getStatistics() { return statistics; }
public void setStatistics(SolverStatistics v) { this.statistics = v; }
}
\ No newline at end of file
package com.aps.macroplanner.output.dto;
import java.util.ArrayList;
import java.util.List;
/**
* 生产任务结果 — 对应 Quintiq 中 PeriodTaskOperation 的回写数据。
*
* <p>每个操作在每个周期的生产量、产能消耗、批次偏差等。</p>
*/
public class PeriodTaskResult {
private String operationId;
private String operationName;
private String unitId;
private int periodIndex;
private String periodStartDate;
/** PTQty — 生产量 */
private double productionQty;
/** 产能消耗 = productionQty × coefficient */
private double capacityUsed;
/** 产能消耗系数 */
private double capacityCoeff;
/** 批次大小 (如有) */
private Double lotSize;
/** 超过批次上限的量 */
private double lotSizeOver;
/** 低于批次下限的量 */
private double lotSizeUnder;
/** 产出: 产品 → 库存点 */
private final List<OutputInfo> outputs = new ArrayList<>();
// ==================== 内嵌类 ====================
/** 产出信息 */
public static class OutputInfo {
public String productId;
public String spId;
public double factor;
}
// ==================== Getters / Setters ====================
public String getOperationId() { return operationId; }
public void setOperationId(String v) { this.operationId = v; }
public String getOperationName() { return operationName; }
public void setOperationName(String v) { this.operationName = v; }
public String getUnitId() { return unitId; }
public void setUnitId(String v) { this.unitId = v; }
public int getPeriodIndex() { return periodIndex; }
public void setPeriodIndex(int v) { this.periodIndex = v; }
public String getPeriodStartDate() { return periodStartDate; }
public void setPeriodStartDate(String v) { this.periodStartDate = v; }
public double getProductionQty() { return productionQty; }
public void setProductionQty(double v) { this.productionQty = v; }
public double getCapacityUsed() { return capacityUsed; }
public void setCapacityUsed(double v) { this.capacityUsed = v; }
public double getCapacityCoeff() { return capacityCoeff; }
public void setCapacityCoeff(double v) { this.capacityCoeff = v; }
public Double getLotSize() { return lotSize; }
public void setLotSize(Double v) { this.lotSize = v; }
public double getLotSizeOver() { return lotSizeOver; }
public void setLotSizeOver(double v) { this.lotSizeOver = v; }
public double getLotSizeUnder() { return lotSizeUnder; }
public void setLotSizeUnder(double v) { this.lotSizeUnder = v; }
public List<OutputInfo> getOutputs() { return outputs; }
}
\ No newline at end of file
package com.aps.macroplanner.output.dto;
import java.util.ArrayList;
import java.util.List;
/**
* 产品生产网络结果 — 从成品向下逐级展开的完整 BOM 供应链视图。
*
* <h3>对应关系</h3>
* <pre>
* ProductNetworkResult —— 成品列表, 每个成品是一棵 BOM 树的根节点
* SupplyChainNode —— 单个产品, 包含: 供应(谁生产)、需求(谁消耗)、库存、子树(消耗的物料)
* SupplySummary —— 跨周期汇总的供应/需求/库存数据
* BomChild —— BOM 子物料链接 (消耗因子 + 数量)
* </pre>
*
* <h3>展开逻辑</h3>
* <pre>
* 1. 找到所有成品 (不被任何工序消耗的产品)
* 2. 对每个成品, 构建 SupplyChainNode
* 3. 对每个供应链节点, 查找消耗它的工序(向上 = 成品/半成品)
* 和它消耗的物料(向下 = BOM子节点)
* 4. 递归展开子节点直到原材料 (无 BOM 输入的工序产出, 或纯采购品)
* </pre>
*/
public class ProductNetworkResult {
/** 成品列表 (BOM 树的根节点) */
private final List<SupplyChainNode> finishedGoods = new ArrayList<>();
/** 所有节点的扁平索引 (按 nodeKey 查找, 用于去重) */
private final java.util.Map<String, SupplyChainNode> allNodes = new java.util.LinkedHashMap<>();
// ==================== Getters ====================
public List<SupplyChainNode> getFinishedGoods() { return finishedGoods; }
public java.util.Map<String, SupplyChainNode> getAllNodes() { return allNodes; }
/** 获取或创建节点 (按 productId@spId 去重) */
public SupplyChainNode getOrCreateNode(String productId, String spId) {
String key = productId + "@" + spId;
return allNodes.computeIfAbsent(key, k -> {
SupplyChainNode node = new SupplyChainNode();
node.setProductId(productId);
node.setSpId(spId);
return node;
});
}
}
\ No newline at end of file
package com.aps.macroplanner.output.dto;
/**
* 销售需求满足结果 — 对应 Quintiq 中 LeafSalesDemandInPeriod 的回写数据。
*
* <p>每个销售需求在每个周期的满足量、缺口、优先级等。</p>
*/
public class SalesDemandResult {
private String salesDemandId;
private String productId;
private String spId;
private int periodIndex;
private String periodStartDate;
/** 需求量 */
private double demandQty;
/** 满足量 (SalesDemandQty) */
private double fulfilledQty;
/** 未满足量 = demandQty - fulfilledQty */
private double unmetQty;
/** 需求松弛 (DemandSlack, 防止不可行) */
private double demandSlack;
/** 满足率 = fulfilledQty / demandQty */
private double fulfillmentRate;
/** 优先级 */
private double priority;
// ==================== Getters / Setters ====================
public String getSalesDemandId() { return salesDemandId; }
public void setSalesDemandId(String v) { this.salesDemandId = v; }
public String getProductId() { return productId; }
public void setProductId(String v) { this.productId = v; }
public String getSpId() { return spId; }
public void setSpId(String v) { this.spId = v; }
public int getPeriodIndex() { return periodIndex; }
public void setPeriodIndex(int v) { this.periodIndex = v; }
public String getPeriodStartDate() { return periodStartDate; }
public void setPeriodStartDate(String v) { this.periodStartDate = v; }
public double getDemandQty() { return demandQty; }
public void setDemandQty(double v) { this.demandQty = v; }
public double getFulfilledQty() { return fulfilledQty; }
public void setFulfilledQty(double v) { this.fulfilledQty = v; }
public double getUnmetQty() { return unmetQty; }
public void setUnmetQty(double v) { this.unmetQty = v; }
public double getDemandSlack() { return demandSlack; }
public void setDemandSlack(double v) { this.demandSlack = v; }
public double getFulfillmentRate() { return fulfillmentRate; }
public void setFulfillmentRate(double v) { this.fulfillmentRate = v; }
public double getPriority() { return priority; }
public void setPriority(double v) { this.priority = v; }
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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; }
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment