Commit bb3dce7b authored by DESKTOP-VKRD9QF\Administration's avatar DESKTOP-VKRD9QF\Administration

合并远端master并保留主计划场景逻辑

parents b0e9cc4a 8ada9aff
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
package com.aps.macroplanner;
import com.aps.macroplanner.data.*;
import java.time.LocalDate;
import java.util.*;
/**
* 大数据量压力测试数据构建器。
*
* <p>基于 MultiLevelBomTestDataBuilder 的数据结构扩展:
* <ul>
* <li>多成品:PRODUCT_COUNT 个</li>
* <li>共享半成品:SEMI_COUNT 个</li>
* <li>多级原材料:RAW_COUNT 个</li>
* <li>多周期:PERIOD_COUNT 天</li>
* <li>每个成品每天生成一个销售需求记录</li>
* <li>成品随机共享半成品;半成品随机消耗 1~3 种原材料</li>
* <li>所有随机数据使用固定 seed,保证每次测试数据完全一致</li>
* </ul>
*
* <p>推荐压力测试规模:
* PRODUCT_COUNT=500, SEMI_COUNT=100, RAW_COUNT=200, PERIOD_COUNT=60
* 时,大约会产生:
* 成品 500 + 半成品 100 + 原材料 200 = 800 个 Product;
* 约 800 个库存点;
* 约 800 个生产/采购工序;
* 约 30,000 条 SalesDemand;
* 数千条 BOM OperationInput。
*
* <p>如果 CP-SAT 模型变量量过大,可先使用 100/200/500 产品逐级压测。
*/
public class LargeScaleBomTestDataBuilder extends TestDataBuilder {
// ============================================================
// 压测规模:只需要修改这里即可生成不同规模
// ============================================================
/** 成品数量 */
private static final int PRODUCT_COUNT = 500;
/** 半成品数量 */
private static final int SEMI_COUNT = 100;
/** 原材料数量 */
private static final int RAW_COUNT = 200;
/** 计划周期 */
private static final int PERIOD_COUNT = 60;
/** 每个成品每天生成 1 条销售需求 */
private static final double MIN_DAILY_DEMAND = 10.0;
private static final double MAX_DAILY_DEMAND = 80.0;
/** 随机种子,保证每次运行生成完全相同的数据 */
private static final long RANDOM_SEED = 20260903L;
/** 每个成品消耗的半成品数量 */
private static final double MIN_SEMI_QTY = 1.0;
private static final double MAX_SEMI_QTY = 3.0;
/** 每个成品直接消耗的原材料种类数 */
private static final int MAX_DIRECT_RAW_TYPES = 2;
/** 每个半成品消耗的原材料种类数 */
private static final int MAX_SEMI_RAW_TYPES = 3;
/** 每个半成品每天最多允许生产数量对应的 UnitPeriod capacity */
private static final double SEMI_DAILY_CAPACITY = 1000.0;
/** 每个成品每天最多生产数量 */
private static final double PRODUCT_DAILY_CAPACITY = 500.0;
/** 每种原材料每天最多采购数量 */
private static final double RAW_DAILY_CAPACITY = 3000.0;
private final Random random = new Random(RANDOM_SEED);
@Override
protected void build() {
// ============================================================
// 1. 周期
// ============================================================
LocalDate startDate = LocalDate.of(2026, 8, 1);
for (int t = 0; t < PERIOD_COUNT; t++) {
periods.add(new Period(
t,
"Day" + (t + 1),
startDate.plusDays(t)
));
}
// ============================================================
// 2. 创建产品
// ============================================================
List<Product> finishedProducts = new ArrayList<>();
List<Product> semiProducts = new ArrayList<>();
List<Product> rawProducts = new ArrayList<>();
for (int i = 1; i <= PRODUCT_COUNT; i++) {
Product p = new Product(
"P" + i,
"成品P" + i
);
finishedProducts.add(p);
products.add(p);
}
for (int i = 1; i <= SEMI_COUNT; i++) {
Product s = new Product(
"S" + i,
"半成品S" + i
);
semiProducts.add(s);
products.add(s);
}
for (int i = 1; i <= RAW_COUNT; i++) {
Product r = new Product(
"R" + i,
"原材料R" + i
);
rawProducts.add(r);
products.add(r);
}
// ============================================================
// 3. 库存点
// ============================================================
Map<String, StockingPoint> spByProduct = new HashMap<>();
for (Product p : products) {
String prefix;
if (p.getCode().startsWith("P")) {
prefix = "成品库";
} else if (p.getCode().startsWith("S")) {
prefix = "半成品库";
} else {
prefix = "原材料库";
}
StockingPoint sp = new StockingPoint(
"SP_" + p.getCode(),
p.getCode() + prefix
);
stockingPoints.add(sp);
productSpMappings.add(new ProductSpMapping(p, sp));
spByProduct.put(p.getCode(), sp);
initialInventories.add(
new InitialInventory(p, sp, 0.0)
);
}
// ============================================================
// 4. 创建生产/采购工序
// ============================================================
Map<String, Operation> operationByProduct = new HashMap<>();
// 成品生产工序
for (Product p : finishedProducts) {
StockingPoint sp = spByProduct.get(p.getCode());
Operation op = new Operation(
"OP_" + p.getCode(),
"生产" + p.getCode(),
"Unit_" + p.getCode(),
new OperationOutput(p, sp),
1.0,
1.0,
false,
0,
1.0
);
operations.add(op);
operationByProduct.put(p.getCode(), op);
}
// 半成品生产工序
for (Product s : semiProducts) {
StockingPoint sp = spByProduct.get(s.getCode());
Operation op = new Operation(
"OP_" + s.getCode(),
"生产" + s.getCode(),
"Unit_" + s.getCode(),
new OperationOutput(s, sp),
1.0,
1.0,
false,
0,
1.0
);
operations.add(op);
operationByProduct.put(s.getCode(), op);
}
// 原材料采购工序
for (Product r : rawProducts) {
StockingPoint sp = spByProduct.get(r.getCode());
Operation op = new Operation(
"OP_Procure_" + r.getCode(),
"采购" + r.getCode(),
"Unit_" + r.getCode(),
new OperationOutput(r, sp),
0.5,
1.0,
false,
0,
1.0
);
operations.add(op);
operationByProduct.put(r.getCode(), op);
}
// ============================================================
// 5. 生成多级 BOM
//
// 结构:
//
// P -> S + Raw
// S -> Raw
//
// 一个 S 可以被多个 P 共享。
// ============================================================
for (int i = 0; i < finishedProducts.size(); i++) {
Product p = finishedProducts.get(i);
Operation opP = operationByProduct.get(p.getCode());
// 为了保证共享半成品:
// 多个成品映射到相同 S。
Product semi = semiProducts.get(
i % semiProducts.size()
);
double semiQty = randomDouble(
MIN_SEMI_QTY,
MAX_SEMI_QTY
);
operationInputs.add(
new OperationInput(
opP,
semi,
spByProduct.get(semi.getCode()),
semiQty
)
);
// 成品直接消耗 0~2 种原材料
int rawTypeCount = random.nextInt(
MAX_DIRECT_RAW_TYPES + 1
);
Set<Integer> selectedRaw = new HashSet<>();
while (selectedRaw.size() < rawTypeCount) {
selectedRaw.add(random.nextInt(RAW_COUNT));
}
for (Integer rawIndex : selectedRaw) {
Product raw = rawProducts.get(rawIndex);
double qty = randomDouble(1.0, 5.0);
operationInputs.add(
new OperationInput(
opP,
raw,
spByProduct.get(raw.getCode()),
qty
)
);
}
}
// 半成品 -> 原材料
for (int i = 0; i < semiProducts.size(); i++) {
Product semi = semiProducts.get(i);
Operation opS = operationByProduct.get(semi.getCode());
// 使用确定性映射,让不同半成品共享部分原材料。
int rawTypeCount = 1 + random.nextInt(MAX_SEMI_RAW_TYPES);
Set<Integer> selectedRaw = new HashSet<>();
// 至少保证一个与自身编号相关的原材料,
// 其余随机选择,从而形成共享原材料网络。
selectedRaw.add(i % RAW_COUNT);
while (selectedRaw.size() < rawTypeCount) {
selectedRaw.add(random.nextInt(RAW_COUNT));
}
for (Integer rawIndex : selectedRaw) {
Product raw = rawProducts.get(rawIndex);
double qty = randomDouble(1.0, 4.0);
operationInputs.add(
new OperationInput(
opS,
raw,
spByProduct.get(raw.getCode()),
qty
)
);
}
}
// ============================================================
// 6. 设备产能
// ============================================================
for (Period period : periods) {
for (Product p : finishedProducts) {
unitPeriods.add(
new UnitPeriod(
"Unit_" + p.getCode(),
"Unit_" + p.getCode(),
period,
0.0,
PRODUCT_DAILY_CAPACITY,
false
)
);
}
for (Product s : semiProducts) {
unitPeriods.add(
new UnitPeriod(
"Unit_" + s.getCode(),
"Unit_" + s.getCode(),
period,
0.0,
SEMI_DAILY_CAPACITY,
false
)
);
}
for (Product r : rawProducts) {
unitPeriods.add(
new UnitPeriod(
"Unit_" + r.getCode(),
"Unit_" + r.getCode(),
period,
0.0,
RAW_DAILY_CAPACITY,
false
)
);
}
}
// ============================================================
// 7. 销售需求
//
// 当前 demo 的 SalesDemand 没有订单号字段,
// 因此这里采用“成品 × 周期”的方式制造大量需求记录。
//
// 如果后续你的模型增加 SalesOrder/OrderLine,
// 可以进一步把每条 Demand 拆成多个订单。
// ============================================================
for (Product p : finishedProducts) {
StockingPoint sp = spByProduct.get(p.getCode());
for (Period period : periods) {
double demand = randomDouble(
MIN_DAILY_DEMAND,
MAX_DAILY_DEMAND
);
salesDemands.add(
new SalesDemand(
p,
sp,
period,
demand,
1.0
)
);
}
}
// ============================================================
// 8. 库存规格
// ============================================================
for (Product p : finishedProducts) {
StockingPoint sp = spByProduct.get(p.getCode());
for (Period period : periods) {
double target = 2.0 * averageDemand();
inventorySpecs.add(
new InventorySpec(
p,
sp,
period,
target,
Math.max(5.0, target * 0.1),
Math.max(100.0, target * 3.0),
true,
true,
true
)
);
}
}
for (Product s : semiProducts) {
StockingPoint sp = spByProduct.get(s.getCode());
for (Period period : periods) {
inventorySpecs.add(
new InventorySpec(
s,
sp,
period,
150.0,
10.0,
1000.0,
true,
true,
true
)
);
}
}
for (Product r : rawProducts) {
StockingPoint sp = spByProduct.get(r.getCode());
for (Period period : periods) {
inventorySpecs.add(
new InventorySpec(
r,
sp,
period,
300.0,
20.0,
3000.0,
true,
true,
true
)
);
}
}
// ============================================================
// 9. 供应规格
// ============================================================
for (Product p : finishedProducts) {
Operation op = operationByProduct.get(p.getCode());
supplySpecs.add(
new SupplySpec(
"Supply-" + p.getCode(),
300.0,
100.0,
1000.0,
true,
Collections.singletonList(op)
)
);
}
for (Product s : semiProducts) {
Operation op = operationByProduct.get(s.getCode());
supplySpecs.add(
new SupplySpec(
"Supply-" + s.getCode(),
1000.0,
300.0,
3000.0,
true,
Collections.singletonList(op)
)
);
}
for (Product r : rawProducts) {
Operation op = operationByProduct.get(r.getCode());
supplySpecs.add(
new SupplySpec(
"Supply-" + r.getCode(),
3000.0,
500.0,
10000.0,
true,
Collections.singletonList(op)
)
);
}
// ============================================================
// 10. KPI 权重
// ============================================================
kpiWeights = new KPIWeights(
100.0,
10.0,
5.0,
5.0,
8.0,
20.0,
8.0,
5.0,
5.0,
1.0,
20.0,
5.0
);
// 控制台输出规模,方便压测时确认数据量
printSummary();
}
private double randomDouble(double min, double max) {
return min + (max - min) * random.nextDouble();
}
private double averageDemand() {
return (MIN_DAILY_DEMAND + MAX_DAILY_DEMAND) / 2.0;
}
private void printSummary() {
System.out.println();
System.out.println("========== Large Scale Test Data ==========");
System.out.println("Periods = " + periods.size());
System.out.println("Products = " + products.size());
System.out.println("StockingPoints = " + stockingPoints.size());
System.out.println("Operations = " + operations.size());
System.out.println("OperationInputs = " + operationInputs.size());
System.out.println("UnitPeriods = " + unitPeriods.size());
System.out.println("SalesDemands = " + salesDemands.size());
System.out.println("InventorySpecs = " + inventorySpecs.size());
System.out.println("SupplySpecs = " + supplySpecs.size());
System.out.println("===========================================");
System.out.println();
}
}
......@@ -87,7 +87,7 @@ public class MacroPlannerOptimizer {
private static final String LOG_DIR = "mp";
/** LP 模型文件路径 */
private static final String LP_FILE_PATH = LOG_DIR + "/lp/model.lp";
private static final String LP_FILE_PATH = LOG_DIR + "/lp/";
/** 运行日志文件路径 */
private static final String LOG_FILE_PATH = LOG_DIR + "/log/";
......@@ -153,6 +153,10 @@ public class MacroPlannerOptimizer {
{
FileHelper.writeFile(msg,LOG_FILE_PATH,"log.txt");
}
private void writeLog(String msg, Object... args)
{
FileHelper.writeFile(String.format(msg,args),LOG_FILE_PATH,"log.txt");
}
// ==================== 模型构建流程 ====================
......@@ -210,12 +214,15 @@ public class MacroPlannerOptimizer {
*/
private void exportLpModel() {
try {
String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))+"-";
Path logDir = Paths.get(LOG_DIR);
if (!Files.exists(logDir)) {
Files.createDirectories(logDir);
}
String lpContent = model.getSolver().exportModelAsLpFormat();
Path lpPath = Paths.get(LP_FILE_PATH).toAbsolutePath();
String lppath=LP_FILE_PATH+date+"model.lp";
Path lpPath = Paths.get(lppath).toAbsolutePath();
Files.write(lpPath, lpContent.getBytes(StandardCharsets.UTF_8));
writeLog(" [OK] LP模型文件已导出: " + lpPath);
} catch (Exception e) {
......@@ -242,7 +249,9 @@ public class MacroPlannerOptimizer {
/** 各层级求解结果 (solve() 填充) */
private final List<LevelResult> levelResults = new ArrayList<>();
public void solve() {
solve("1");
}
/**
* 执行分层优化求解。
*
......@@ -260,82 +269,132 @@ public class MacroPlannerOptimizer {
* <p>该实现对应 Quintiq 中 StrategyLevel 的 HierarchicalSolver 机制。
* 每个层级独立求解, 上层最优值作为下层约束, 确保严格优先级顺序。</p>
*/
public void solve() {
public void solve(String sceneId) {
writeLog("=== 开始分层求解 ===\n");
startTimeMs = System.currentTimeMillis();
final long startTimeMs = System.currentTimeMillis();
// ========== 1. 前置参数校验 ==========
KPIWeights w = data.getKpiWeights();
if (w == null) {
writeLog("[ERROR] KPI权重配置为空,终止求解");
return;
}
List<StrategyLevel> levels = defineLevels(w);
levelResults.clear();
for (int i = 0; i < levels.size(); i++) {
StrategyLevel level = levels.get(i);
if (!level.hasKpis()) continue;
System.out.printf("--- 第 %d/%d 层: %s (松弛=%.0f%%) ---%n",
i + 1, levels.size(), level.getName(),
level.getRelativeGoalSlack() * 100);
// 清除上层目标, 设置当前层目标
ObjectiveBuilder.clearObjective(model);
ObjectiveBuilder.setLevelObjective(model, level);
// 求解 (计时)
long levelStartMs = System.currentTimeMillis();
final MPSolver.ResultStatus status = model.getSolver().solve();
long levelElapsedMs = System.currentTimeMillis() - levelStartMs;
double optimalValue = model.getSolver().objective().value();
// SCIP 风格求解摘要
System.out.printf(" SCIP Status : %s%n", status);
System.out.printf(" Solving Time (sec) : %.2f%n", levelElapsedMs / 1000.0);
System.out.printf(" Primal Bound : %+.6e%n", optimalValue);
// 输出当前层各 KPI 值
for (StrategyLevel.KPIEntry kpi : level.getKpis()) {
double kpiValue = kpi.variable.solutionValue();
double penalty = kpi.effectiveCoefficient() * kpiValue;
System.out.printf(" %s: %.2f (系数=%.1f, 惩罚=%.2f)%n",
kpi.name, kpiValue, kpi.effectiveCoefficient(), penalty);
}
levelResults.add(new LevelResult(level, optimalValue, status));
// 添加边界约束 (最后一层不需要)
if (i < levels.size() - 1 && level.getRelativeGoalSlack() >= 0.0) {
ObjectiveBuilder.addLevelBoundConstraint(model, level, optimalValue);
System.out.printf(" 已添加边界约束: 上层目标 ≤ %.2f%n",
ObjectiveBuilder.computeUpperBound(optimalValue, level.getRelativeGoalSlack()));
}
writeLog("-----------------------------------");
if (levels == null || levels.isEmpty()) {
writeLog("[ERROR] 分层策略为空,终止求解");
return;
}
// 输出最终结果
MPSolver.ResultStatus finalStatus = model.getSolver().solve();
if (finalStatus == MPSolver.ResultStatus.OPTIMAL
|| finalStatus == MPSolver.ResultStatus.FEASIBLE) {
SolutionPrinter printer = new SolutionPrinter(model, data, startTimeMs,LOG_FILE_PATH);
// 改为方法内局部变量,避免并发冲突与数据污染
List<LevelResult> levelResults = new ArrayList<>(levels.size());
printer.printAll();
// 构建层级最优值列表
List<Double> levelObjValues = new ArrayList<>();
for (LevelResult r : levelResults) {
levelObjValues.add(r.optimalValue);
try {
// ========== 2. 分层递进求解 ==========
for (int i = 0; i < levels.size(); i++) {
StrategyLevel level = levels.get(i);
double slack = level.getRelativeGoalSlack();
// 无KPI层级直接跳过
if (!level.hasKpis()) {
writeLog("--- 第 %d/%d 层: %s 无KPI,跳过 ---%n", i + 1, levels.size(), level.getName());
levelResults.add(new LevelResult(level, 0.0, MPSolver.ResultStatus.FEASIBLE));
continue;
}
writeLog("--- 第 %d/%d 层: %s (松弛=%.0f%%) ---%n",
i + 1, levels.size(), level.getName(), slack * 100);
// 松弛率合法性校验(负松弛数学上不可满足)
if (slack < 0.0) {
writeLog("[ERROR] 第 %d 层松弛率为负(%.2f%%),非法参数,终止求解", i + 1, slack * 100);
return;
}
// 重置目标函数,保留之前所有约束
ObjectiveBuilder.clearObjective(model);
ObjectiveBuilder.setLevelObjective(model, level);
// 求解计时
long levelStartMs = System.currentTimeMillis();
final MPSolver.ResultStatus status = model.getSolver().solve();
long levelElapsedMs = System.currentTimeMillis() - levelStartMs;
// ========== 核心修复:求解状态强校验 ==========
if (status != MPSolver.ResultStatus.OPTIMAL
&& status != MPSolver.ResultStatus.FEASIBLE) {
writeLog("[ERROR] 第 %d 层求解失败,状态: %s,终止分层求解%n", i + 1, status);
writeLog("-----------------------------------");
return;
}
double optimalValue = model.getSolver().objective().value();
double bestBound = model.getSolver().objective().bestBound();
writeLog(" SCIP Status : %s%n", status);
writeLog(" Solving Time (sec) : %.2f%n", levelElapsedMs / 1000.0);
writeLog(" Primal Bound : %+.6e%n", optimalValue);
writeLog(" Best Bound : %+.6e%n", bestBound);
double gap = Math.abs(optimalValue - bestBound) / Math.abs(optimalValue) * 100;
writeLog("gap:%.2f", gap);
// 输出当前层各KPI值
for (StrategyLevel.KPIEntry kpi : level.getKpis()) {
double kpiValue = kpi.variable.solutionValue();
double penalty = kpi.effectiveCoefficient() * kpiValue;
writeLog(" %s: %.2f (系数=%.1f, 惩罚=%.2f)%n",
kpi.name, kpiValue, kpi.effectiveCoefficient(), penalty);
}
levelResults.add(new LevelResult(level, optimalValue, status));
// 添加边界约束(最后一层不需要)
if (i < levels.size() - 1) {
double upperBound = ObjectiveBuilder.computeUpperBound(optimalValue, slack);
ObjectiveBuilder.addLevelBoundConstraint(model, level, optimalValue);
writeLog(" 已添加边界约束: 上层目标 ≤ %.2f%n", upperBound);
}
writeLog("-----------------------------------");
}
printer.printHierarchicalSummary(levels, levelObjValues);
// 回写业务对象到 JSON 文件
ResultWriter rw = new ResultWriter(model, data, startTimeMs);
boolean jsonPath = rw.saveResultToFile("1");
if (jsonPath) {
writeLog("\n[OK] 优化结果JSON已导出: " + jsonPath);
// ========== 3. 最终结果输出 ==========
// 优化:移除循环外冗余的 solve(),直接复用最后一层结果
MPSolver.ResultStatus finalStatus = levelResults.get(levelResults.size() - 1).status;
if (finalStatus == MPSolver.ResultStatus.OPTIMAL
|| finalStatus == MPSolver.ResultStatus.FEASIBLE) {
SolutionPrinter printer = new SolutionPrinter(model, data, startTimeMs, LOG_FILE_PATH);
printer.printAll();
// ========== 核心修复:基于最终解重算各层实际目标值 ==========
// 原因:后层优化会使前层目标在松弛范围内变差,单层最优值 ≠ 最终解实际值
List<Double> finalLevelValues = new ArrayList<>(levels.size());
for (StrategyLevel level : levels) {
if (!level.hasKpis()) {
finalLevelValues.add(0.0);
continue;
}
double levelActualValue = 0.0;
for (StrategyLevel.KPIEntry kpi : level.getKpis()) {
levelActualValue += kpi.effectiveCoefficient() * kpi.variable.solutionValue();
}
finalLevelValues.add(levelActualValue);
}
printer.printHierarchicalSummary(levels, finalLevelValues);
// 回写业务对象到JSON文件(预留)
// ResultWriter rw = new ResultWriter(model, data, startTimeMs);
// boolean jsonPath = rw.saveResultToFile(sceneId);
// if (jsonPath) {
// writeLog("\n[OK] 优化结果JSON已导出: " + jsonPath);
// }
writeLog("\n=== 分层求解完成 ===");
} else {
writeLog("求解失败! 最终状态: " + finalStatus);
}
} else {
writeLog("求解失败! 状态: " + finalStatus);
} catch (Exception e) {
// 全局异常捕获,避免求解器异常导致进程崩溃
writeLog("[FATAL] 分层求解过程发生异常: %s%n%s", e.getMessage(), e.getStackTrace());
}
}
......
package com.aps.macroplanner;
import com.aps.common.util.FileHelper;
import com.aps.macroplanner.data.BenchmarkDataBuilder;
import com.aps.macroplanner.data.LargeScaleBomTestDataBuilder;
import com.google.ortools.Loader;
import com.aps.macroplanner.data.MultiLevelBomTestDataBuilder;
import com.aps.macroplanner.data.TestDataBuilder;
......@@ -23,6 +26,18 @@ import com.aps.macroplanner.data.TestDataBuilder;
* </ol>
*/
public class MultiLevelBomTestRunner {
private static final String LOG_DIR = "mp";
/** LP 模型文件路径 */
/** 运行日志文件路径 */
private static final String LOG_FILE_PATH = LOG_DIR + "/log/";
private static void writeLog(String msg)
{
FileHelper.writeFile(msg,LOG_FILE_PATH,"log.txt");
}
public static void main(String[] args) {
Loader.loadNativeLibraries();
......@@ -31,12 +46,21 @@ public class MultiLevelBomTestRunner {
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 =====");
for (int scale : BenchmarkDataBuilder.SUPPORTED_SCALES) {
BenchmarkDataBuilder data =
BenchmarkDataBuilder.forScale(scale);
writeLog("===== TEST RUNNER START "+scale+"=====");
data.init();
writeLog("Data loaded: " + data.getProducts().size() + " products, "
+ data.getOperations().size() + " operations");
MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
optimizer.buildModel();
optimizer.solve(String.valueOf(scale));
writeLog("===== TEST RUNNER END =====");
}
}
}
\ No newline at end of file
......@@ -53,8 +53,8 @@ public class BalanceConstraint {
}
}
// DemandSlack
MPVariable slackVar = demandSlackVars.get(invKey);
if (slackVar != null) balance.setCoefficient(slackVar, 1.0);
// MPVariable slackVar = demandSlackVars.get(invKey);
// if (slackVar != null) balance.setCoefficient(slackVar, 1.0);
// 上一周期库存 (t>0)
if (p.getIndex() > 0) {
String prevKey = prod.getId() + "_" + sp.getId() + "_" + (p.getIndex() - 1);
......
package com.aps.macroplanner.constraint;
import com.aps.common.util.FileHelper;
import com.aps.macroplanner.data.TestDataBuilder;
import com.aps.macroplanner.model.MacroPlannerModel;
......@@ -117,7 +118,21 @@ public class ConstraintFactory {
logSummary(model);
}
private static final String LOG_DIR = "mp";
/** LP 模型文件路径 */
private static final String LP_FILE_PATH = LOG_DIR + "/lp/";
/** 运行日志文件路径 */
private static final String LOG_FILE_PATH = LOG_DIR + "/log/";
private static void writeLog(String msg)
{
FileHelper.writeFile(msg,LOG_FILE_PATH,"log.txt");
}
private static void writeLog(String msg, Object... args)
{
FileHelper.writeFile(String.format(msg,args),LOG_FILE_PATH,"log.txt");
}
/**
* 记录单个约束构建步骤的结果。
* 输出新增约束数和新增变量数, 如果为 0 则输出 WARNING。
......@@ -129,7 +144,7 @@ public class ConstraintFactory {
int addedVariables = model.getSolver().numVariables() - beforeVariables;
if (addedConstraints == 0 && addedVariables == 0) {
LOG.warning(String.format("[%s] %s → 未创建任何约束或变量! (可能数据为空)",
writeLog(String.format("[%s] %s → 未创建任何约束或变量! (可能数据为空)",
stepName, description));
} else {
StringBuilder sb = new StringBuilder();
......@@ -140,7 +155,7 @@ public class ConstraintFactory {
if (addedVariables > 0) {
sb.append(String.format(" | +%d变量", addedVariables));
}
LOG.info(sb.toString());
writeLog(sb.toString());
}
}
......
package com.aps.macroplanner.data;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
/**
* MacroPlanner 大数据 Benchmark 数据构建器。
*
* 六档成品规模:100 / 200 / 500 / 1000 / 2000 / 5000。
* 与车间详细排产无关。
*
* 固定:30 个计划周期、固定 RANDOM_SEED、成品:半成品:原材料 = 5:1:2。
* 每一档从相同 Seed 重新生成,保证同一档可重复、不同档生成规则一致。
*/
public class BenchmarkDataBuilder extends TestDataBuilder {
public static final int[] SUPPORTED_SCALES = {100, 200, 500, 1000, 2000, 5000};
public static final long RANDOM_SEED = 20260826L;
public static final int PERIOD_COUNT = 15;
private static final int SEMI_DIVISOR = 5;
private static final int RAW_MULTIPLIER = 2;
/*
* 兼容当前 TestDataBuilder 的构造模式:如果父类构造函数调用 build(),
* 普通实例字段尚未初始化。ThreadLocal 可在 build() 执行期间安全传递规模。
*/
private static Integer CONSTRUCTION_SCALE = 100;
private final Map<Product, String> productCodes = new HashMap<>();
/** 创建指定规模的数据。仅允许六档正式 Benchmark 规模。 */
public static BenchmarkDataBuilder forScale(int finishedProductCount) {
validateScale(finishedProductCount);
CONSTRUCTION_SCALE=finishedProductCount;
try {
return new BenchmarkDataBuilder();
} finally {
// CONSTRUCTION_SCALE.remove();
}
}
/** 六档规模。 */
public static List<Integer> scales() {
List<Integer> result = new ArrayList<>();
for (int scale : SUPPORTED_SCALES) result.add(scale);
return Collections.unmodifiableList(result);
}
public int getFinishedProductCount() {
return salesDemands.size() / PERIOD_COUNT;
}
public int getSemiProductCount() {
return getFinishedProductCount() / SEMI_DIVISOR;
}
public int getRawMaterialCount() {
return getFinishedProductCount() * RAW_MULTIPLIER / SEMI_DIVISOR;
}
public int getTotalProductCount() {
return products.size();
}
@Override
protected void build() {
Integer scale = CONSTRUCTION_SCALE;
int finishedCount = scale == null ? 100 : scale;
validateScale(finishedCount);
// 每一档都重新使用相同 Seed,保证结果可重复、便于横向比较。
Random random = new Random(RANDOM_SEED);
int semiCount = finishedCount / SEMI_DIVISOR;
int rawCount = finishedCount * RAW_MULTIPLIER / SEMI_DIVISOR;
buildPeriods();
ProductGroups groups = buildProducts(finishedCount, semiCount, rawCount);
buildStockingPoints(groups);
buildMappings(groups);
OperationGroups ops = buildOperations(groups);
buildBom(groups, ops, random);
buildUnitPeriods(groups);
buildInitialInventory(groups);
buildSalesDemand(groups, random);
buildInventorySpecs(groups);
buildSupplySpecs(groups, ops);
buildKpiWeights();
}
private static void validateScale(int scale) {
for (int supported : SUPPORTED_SCALES) {
if (supported == scale) return;
}
throw new IllegalArgumentException(
"Unsupported benchmark scale: " + scale
+ ". Supported: 100, 200, 500, 1000, 2000, 5000");
}
private void buildPeriods() {
LocalDate start = LocalDate.of(2026, 8, 7);
for (int i = 0; i < PERIOD_COUNT; i++) {
periods.add(new Period(i, "Day" + (i + 1), start.plusDays(i)));
}
}
private ProductGroups buildProducts(int finishedCount, int semiCount, int rawCount) {
ProductGroups g = new ProductGroups();
for (int i = 0; i < finishedCount; i++) {
Product p = new Product("P" + (i + 1), "成品P" + (i + 1));
g.finished.add(p); products.add(p); productCodes.put(p, "P" + (i + 1));
}
for (int i = 0; i < semiCount; i++) {
Product p = new Product("S" + (i + 1), "半成品S" + (i + 1));
g.semi.add(p); products.add(p); productCodes.put(p, "S" + (i + 1));
}
for (int i = 0; i < rawCount; i++) {
Product p = new Product("R" + (i + 1), "原材料R" + (i + 1));
g.raw.add(p); products.add(p); productCodes.put(p, "R" + (i + 1));
}
return g;
}
private void buildStockingPoints(ProductGroups g) {
for (Product p : products) {
String code = productCodes.get(p);
StockingPoint sp = new StockingPoint("SP_" + code, code + "库存点");
g.stockingPointByProduct.put(p, sp);
stockingPoints.add(sp);
}
}
private void buildMappings(ProductGroups g) {
for (Product p : products) {
productSpMappings.add(new ProductSpMapping(p, g.stockingPointByProduct.get(p)));
}
}
private OperationGroups buildOperations(ProductGroups g) {
OperationGroups result = new OperationGroups();
for (Product p : g.finished) result.productionByProduct.put(p, addProductionOperation(p, g));
for (Product p : g.semi) result.productionByProduct.put(p, addProductionOperation(p, g));
for (Product p : g.raw) result.procurementByProduct.put(p, addProcurementOperation(p, g));
return result;
}
private Operation addProductionOperation(Product p, ProductGroups g) {
String code = productCodes.get(p);
Operation op = new Operation(
"OP_" + code, "生产" + code, "Unit_" + code,
new OperationOutput(p, g.stockingPointByProduct.get(p)),
1.0, 1.0, false, 0, 1.0);
operations.add(op);
return op;
}
private Operation addProcurementOperation(Product p, ProductGroups g) {
String code = productCodes.get(p);
Operation op = new Operation(
"OP_Procure_" + code, "采购" + code, "Unit_" + code,
new OperationOutput(p, g.stockingPointByProduct.get(p)),
0.5, 1.0, false, 0, 1.0);
operations.add(op);
return op;
}
/**
* 多级共享 BOM:
* P -> S + Raw;S -> Raw。
* 每个成品至少消耗一个半成品,因此存在共享半成品网络。
*/
private void buildBom(ProductGroups g, OperationGroups ops, Random random) {
int rawCount = g.raw.size();
for (int i = 0; i < g.finished.size(); i++) {
Product finished = g.finished.get(i);
Operation op = ops.productionByProduct.get(finished);
Product semi = g.semi.get(i % g.semi.size());
double semiQty = 1.0 + random.nextInt(3); // 1~3
operationInputs.add(new OperationInput(
op, semi, g.stockingPointByProduct.get(semi), semiQty));
if (random.nextDouble() < 0.70) {
Product raw = g.raw.get(stableIndex(i, 17, rawCount));
double rawQty = 1.0 + random.nextInt(5); // 1~5
operationInputs.add(new OperationInput(
op, raw, g.stockingPointByProduct.get(raw), rawQty));
}
}
for (int i = 0; i < g.semi.size(); i++) {
Product semi = g.semi.get(i);
Operation op = ops.productionByProduct.get(semi);
Product raw1 = g.raw.get(stableIndex(i, 31, rawCount));
double qty1 = 1.0 + random.nextInt(3); // 1~3
operationInputs.add(new OperationInput(
op, raw1, g.stockingPointByProduct.get(raw1), qty1));
if (random.nextDouble() < 0.55) {
Product raw2 = g.raw.get(stableIndex(i, 47, rawCount));
double qty2 = 1.0 + random.nextInt(2); // 1~2
operationInputs.add(new OperationInput(
op, raw2, g.stockingPointByProduct.get(raw2), qty2));
}
}
}
private int stableIndex(int i, int salt, int size) {
long x = i * 1103515245L + salt * 12345L + RANDOM_SEED;
x ^= (x >>> 16);
return Math.floorMod((int) x, size);
}
private void buildUnitPeriods(ProductGroups g) {
for (Product p : products) {
String code = productCodes.get(p);
double capacity;
if (g.finished.contains(p)) capacity = 500.0;
else if (g.semi.contains(p)) capacity = 1500.0;
else capacity = 3000.0;
for (Period period : periods) {
unitPeriods.add(new UnitPeriod(
"Unit_" + code, "Unit_" + code,
period, 0.0, capacity, false));
}
}
}
private void buildInitialInventory(ProductGroups g) {
for (Product p : products) {
initialInventories.add(new InitialInventory(
p, g.stockingPointByProduct.get(p), 0.0));
}
}
/** 每个成品每周期一条需求,需求量固定 Seed 随机生成 20~80。 */
private void buildSalesDemand(ProductGroups g, Random random) {
for (Product p : g.finished) {
StockingPoint sp = g.stockingPointByProduct.get(p);
for (Period period : periods) {
double demand = 20.0 + random.nextInt(61);
salesDemands.add(new SalesDemand(p, sp, period, demand, 1.0));
}
}
}
private void buildInventorySpecs(ProductGroups g) {
for (Product p : products) {
StockingPoint sp = g.stockingPointByProduct.get(p);
double target, min, max;
if (g.finished.contains(p)) {
target = 60.0; min = 10.0; max = 250.0;
} else if (g.semi.contains(p)) {
target = 180.0; min = 20.0; max = 800.0;
} else {
target = 300.0; min = 30.0; max = 3000.0;
}
for (Period period : periods) {
inventorySpecs.add(new InventorySpec(
p, sp, period, target, min, max, true, true, true));
}
}
}
private void buildSupplySpecs(ProductGroups g, OperationGroups ops) {
for (Product p : g.finished) {
supplySpecs.add(new SupplySpec(
"Supply-" + productCodes.get(p), 150.0, 60.0, 500.0, true,
Collections.singletonList(ops.productionByProduct.get(p))));
}
for (Product p : g.semi) {
supplySpecs.add(new SupplySpec(
"Supply-" + productCodes.get(p), 500.0, 200.0, 1500.0, true,
Collections.singletonList(ops.productionByProduct.get(p))));
}
for (Product p : g.raw) {
supplySpecs.add(new SupplySpec(
"Supply-" + productCodes.get(p), 1000.0, 300.0, 3000.0, true,
Collections.singletonList(ops.procurementByProduct.get(p))));
}
}
private void buildKpiWeights() {
// 与原 MultiLevelBomTestDataBuilder 保持一致。
kpiWeights = new KPIWeights(
100.0, 10.0, 5.0, 5.0, 8.0, 20.0,
8.0, 5.0, 5.0, 1.0, 20.0, 5.0);
}
private static class ProductGroups {
final List<Product> finished = new ArrayList<>();
final List<Product> semi = new ArrayList<>();
final List<Product> raw = new ArrayList<>();
final Map<Product, StockingPoint> stockingPointByProduct = new HashMap<>();
}
private static class OperationGroups {
final Map<Product, Operation> productionByProduct = new HashMap<>();
final Map<Product, Operation> procurementByProduct = new HashMap<>();
}
}
package com.aps.macroplanner.data;
import java.util.Arrays;
/** 六档 MacroPlanner Benchmark 规模。 */
public final class BenchmarkScales {
private BenchmarkScales() {}
public static int[] all() {
return Arrays.copyOf(
BenchmarkDataBuilder.SUPPORTED_SCALES,
BenchmarkDataBuilder.SUPPORTED_SCALES.length);
}
/** 顺序创建六档数据,方便现有 Benchmark/性能测试接入。 */
public static void forEach(java.util.function.IntFunction<BenchmarkDataBuilder> consumer) {
for (int scale : all()) consumer.apply(scale);
}
}
package com.aps.macroplanner.data;
import java.time.LocalDate;
import java.util.*;
/**
* 大数据量压力测试数据构建器。
*
* <p>基于 MultiLevelBomTestDataBuilder 的数据结构扩展:
* <ul>
* <li>多成品:PRODUCT_COUNT 个</li>
* <li>共享半成品:SEMI_COUNT 个</li>
* <li>多级原材料:RAW_COUNT 个</li>
* <li>多周期:PERIOD_COUNT 天</li>
* <li>每个成品每天生成一个销售需求记录</li>
* <li>成品随机共享半成品;半成品随机消耗 1~3 种原材料</li>
* <li>所有随机数据使用固定 seed,保证每次测试数据完全一致</li>
* </ul>
*
* <p>推荐压力测试规模:
* PRODUCT_COUNT=500, SEMI_COUNT=100, RAW_COUNT=200, PERIOD_COUNT=60
* 时,大约会产生:
* 成品 500 + 半成品 100 + 原材料 200 = 800 个 Product;
* 约 800 个库存点;
* 约 800 个生产/采购工序;
* 约 30,000 条 SalesDemand;
* 数千条 BOM OperationInput。
*
* <p>如果 CP-SAT 模型变量量过大,可先使用 100/200/500 产品逐级压测。
*/
public class LargeScaleBomTestDataBuilder extends TestDataBuilder {
// ============================================================
// 压测规模:只需要修改这里即可生成不同规模
// ============================================================
/** 成品数量 */
private static final int PRODUCT_COUNT = 100;
/** 半成品数量 */
private static final int SEMI_COUNT = 20;
/** 原材料数量 */
private static final int RAW_COUNT = 40;
/** 计划周期 */
private static final int PERIOD_COUNT = 30;
/** 每个成品每天生成 1 条销售需求 */
private static final double MIN_DAILY_DEMAND = 10.0;
private static final double MAX_DAILY_DEMAND = 80.0;
/** 随机种子,保证每次运行生成完全相同的数据 */
private static final long RANDOM_SEED = 20260903L;
/** 每个成品消耗的半成品数量 */
private static final double MIN_SEMI_QTY = 1.0;
private static final double MAX_SEMI_QTY = 3.0;
/** 每个成品直接消耗的原材料种类数 */
private static final int MAX_DIRECT_RAW_TYPES = 2;
/** 每个半成品消耗的原材料种类数 */
private static final int MAX_SEMI_RAW_TYPES = 3;
/** 每个半成品每天最多允许生产数量对应的 UnitPeriod capacity */
private static final double SEMI_DAILY_CAPACITY = 1000.0;
/** 每个成品每天最多生产数量 */
private static final double PRODUCT_DAILY_CAPACITY = 500.0;
/** 每种原材料每天最多采购数量 */
private static final double RAW_DAILY_CAPACITY = 3000.0;
private final Random random = new Random(RANDOM_SEED);
@Override
protected void build() {
// ============================================================
// 1. 周期
// ============================================================
LocalDate startDate = LocalDate.of(2026, 8, 1);
for (int t = 0; t < PERIOD_COUNT; t++) {
periods.add(new Period(
t,
"Day" + (t + 1),
startDate.plusDays(t)
));
}
// ============================================================
// 2. 创建产品
// ============================================================
List<Product> finishedProducts = new ArrayList<>();
List<Product> semiProducts = new ArrayList<>();
List<Product> rawProducts = new ArrayList<>();
for (int i = 1; i <= PRODUCT_COUNT; i++) {
Product p = new Product(
"P" + i,
"成品P" + i
);
finishedProducts.add(p);
products.add(p);
}
for (int i = 1; i <= SEMI_COUNT; i++) {
Product s = new Product(
"S" + i,
"半成品S" + i
);
semiProducts.add(s);
products.add(s);
}
for (int i = 1; i <= RAW_COUNT; i++) {
Product r = new Product(
"R" + i,
"原材料R" + i
);
rawProducts.add(r);
products.add(r);
}
// ============================================================
// 3. 库存点
// ============================================================
Map<String, StockingPoint> spByProduct = new HashMap<>();
for (Product p : products) {
String prefix;
if (p.getCode().startsWith("P")) {
prefix = "成品库";
} else if (p.getCode().startsWith("S")) {
prefix = "半成品库";
} else {
prefix = "原材料库";
}
StockingPoint sp = new StockingPoint(
"SP_" + p.getCode(),
p.getCode() + prefix
);
stockingPoints.add(sp);
productSpMappings.add(new ProductSpMapping(p, sp));
spByProduct.put(p.getCode(), sp);
initialInventories.add(
new InitialInventory(p, sp, 0.0)
);
}
// ============================================================
// 4. 创建生产/采购工序
// ============================================================
Map<String, Operation> operationByProduct = new HashMap<>();
// 成品生产工序
for (Product p : finishedProducts) {
StockingPoint sp = spByProduct.get(p.getCode());
Operation op = new Operation(
"OP_" + p.getCode(),
"生产" + p.getCode(),
"Unit_" + p.getCode(),
new OperationOutput(p, sp),
1.0,
1.0,
false,
0,
1.0
);
operations.add(op);
operationByProduct.put(p.getCode(), op);
}
// 半成品生产工序
for (Product s : semiProducts) {
StockingPoint sp = spByProduct.get(s.getCode());
Operation op = new Operation(
"OP_" + s.getCode(),
"生产" + s.getCode(),
"Unit_" + s.getCode(),
new OperationOutput(s, sp),
1.0,
1.0,
false,
0,
1.0
);
operations.add(op);
operationByProduct.put(s.getCode(), op);
}
// 原材料采购工序
for (Product r : rawProducts) {
StockingPoint sp = spByProduct.get(r.getCode());
Operation op = new Operation(
"OP_Procure_" + r.getCode(),
"采购" + r.getCode(),
"Unit_" + r.getCode(),
new OperationOutput(r, sp),
0.5,
1.0,
false,
0,
1.0
);
operations.add(op);
operationByProduct.put(r.getCode(), op);
}
// ============================================================
// 5. 生成多级 BOM
//
// 结构:
//
// P -> S + Raw
// S -> Raw
//
// 一个 S 可以被多个 P 共享。
// ============================================================
for (int i = 0; i < finishedProducts.size(); i++) {
Product p = finishedProducts.get(i);
Operation opP = operationByProduct.get(p.getCode());
// 为了保证共享半成品:
// 多个成品映射到相同 S。
Product semi = semiProducts.get(
i % semiProducts.size()
);
double semiQty = randomDouble(
MIN_SEMI_QTY,
MAX_SEMI_QTY
);
operationInputs.add(
new OperationInput(
opP,
semi,
spByProduct.get(semi.getCode()),
semiQty
)
);
// 成品直接消耗 0~2 种原材料
int rawTypeCount = random.nextInt(
MAX_DIRECT_RAW_TYPES + 1
);
Set<Integer> selectedRaw = new HashSet<>();
while (selectedRaw.size() < rawTypeCount) {
selectedRaw.add(random.nextInt(RAW_COUNT));
}
for (Integer rawIndex : selectedRaw) {
Product raw = rawProducts.get(rawIndex);
double qty = randomDouble(1.0, 5.0);
operationInputs.add(
new OperationInput(
opP,
raw,
spByProduct.get(raw.getCode()),
qty
)
);
}
}
// 半成品 -> 原材料
for (int i = 0; i < semiProducts.size(); i++) {
Product semi = semiProducts.get(i);
Operation opS = operationByProduct.get(semi.getCode());
// 使用确定性映射,让不同半成品共享部分原材料。
int rawTypeCount = 1 + random.nextInt(MAX_SEMI_RAW_TYPES);
Set<Integer> selectedRaw = new HashSet<>();
// 至少保证一个与自身编号相关的原材料,
// 其余随机选择,从而形成共享原材料网络。
selectedRaw.add(i % RAW_COUNT);
while (selectedRaw.size() < rawTypeCount) {
selectedRaw.add(random.nextInt(RAW_COUNT));
}
for (Integer rawIndex : selectedRaw) {
Product raw = rawProducts.get(rawIndex);
double qty = randomDouble(1.0, 4.0);
operationInputs.add(
new OperationInput(
opS,
raw,
spByProduct.get(raw.getCode()),
qty
)
);
}
}
// ============================================================
// 6. 设备产能
// ============================================================
for (Period period : periods) {
for (Product p : finishedProducts) {
unitPeriods.add(
new UnitPeriod(
"Unit_" + p.getCode(),
"Unit_" + p.getCode(),
period,
0.0,
PRODUCT_DAILY_CAPACITY,
false
)
);
}
for (Product s : semiProducts) {
unitPeriods.add(
new UnitPeriod(
"Unit_" + s.getCode(),
"Unit_" + s.getCode(),
period,
0.0,
SEMI_DAILY_CAPACITY,
false
)
);
}
for (Product r : rawProducts) {
unitPeriods.add(
new UnitPeriod(
"Unit_" + r.getCode(),
"Unit_" + r.getCode(),
period,
0.0,
RAW_DAILY_CAPACITY,
false
)
);
}
}
// ============================================================
// 7. 销售需求
//
// 当前 demo 的 SalesDemand 没有订单号字段,
// 因此这里采用“成品 × 周期”的方式制造大量需求记录。
//
// 如果后续你的模型增加 SalesOrder/OrderLine,
// 可以进一步把每条 Demand 拆成多个订单。
// ============================================================
for (Product p : finishedProducts) {
StockingPoint sp = spByProduct.get(p.getCode());
for (Period period : periods) {
double demand = randomDouble(
MIN_DAILY_DEMAND,
MAX_DAILY_DEMAND
);
salesDemands.add(
new SalesDemand(
p,
sp,
period,
demand,
1.0
)
);
}
}
// ============================================================
// 8. 库存规格
// ============================================================
for (Product p : finishedProducts) {
StockingPoint sp = spByProduct.get(p.getCode());
for (Period period : periods) {
double target = 2.0 * averageDemand();
inventorySpecs.add(
new InventorySpec(
p,
sp,
period,
target,
Math.max(5.0, target * 0.1),
Math.max(100.0, target * 3.0),
true,
true,
true
)
);
}
}
for (Product s : semiProducts) {
StockingPoint sp = spByProduct.get(s.getCode());
for (Period period : periods) {
inventorySpecs.add(
new InventorySpec(
s,
sp,
period,
150.0,
10.0,
1000.0,
true,
true,
true
)
);
}
}
for (Product r : rawProducts) {
StockingPoint sp = spByProduct.get(r.getCode());
for (Period period : periods) {
inventorySpecs.add(
new InventorySpec(
r,
sp,
period,
300.0,
20.0,
3000.0,
true,
true,
true
)
);
}
}
// ============================================================
// 9. 供应规格
// ============================================================
for (Product p : finishedProducts) {
Operation op = operationByProduct.get(p.getCode());
supplySpecs.add(
new SupplySpec(
"Supply-" + p.getCode(),
300.0,
100.0,
1000.0,
true,
Collections.singletonList(op)
)
);
}
for (Product s : semiProducts) {
Operation op = operationByProduct.get(s.getCode());
supplySpecs.add(
new SupplySpec(
"Supply-" + s.getCode(),
1000.0,
300.0,
3000.0,
true,
Collections.singletonList(op)
)
);
}
for (Product r : rawProducts) {
Operation op = operationByProduct.get(r.getCode());
supplySpecs.add(
new SupplySpec(
"Supply-" + r.getCode(),
3000.0,
500.0,
10000.0,
true,
Collections.singletonList(op)
)
);
}
// ============================================================
// 10. KPI 权重
// ============================================================
kpiWeights = new KPIWeights(
100.0,
10.0,
5.0,
5.0,
8.0,
20.0,
8.0,
5.0,
5.0,
1.0,
20.0,
5.0
);
// 控制台输出规模,方便压测时确认数据量
printSummary();
}
private double randomDouble(double min, double max) {
return min + (max - min) * random.nextDouble();
}
private double averageDemand() {
return (MIN_DAILY_DEMAND + MAX_DAILY_DEMAND) / 2.0;
}
private void printSummary() {
System.out.println();
System.out.println("========== Large Scale Test Data ==========");
System.out.println("Periods = " + periods.size());
System.out.println("Products = " + products.size());
System.out.println("StockingPoints = " + stockingPoints.size());
System.out.println("Operations = " + operations.size());
System.out.println("OperationInputs = " + operationInputs.size());
System.out.println("UnitPeriods = " + unitPeriods.size());
System.out.println("SalesDemands = " + salesDemands.size());
System.out.println("InventorySpecs = " + inventorySpecs.size());
System.out.println("SupplySpecs = " + supplySpecs.size());
System.out.println("===========================================");
System.out.println();
}
}
......@@ -165,9 +165,10 @@ public class MacroPlannerDataConverter {
// 0. 读取时间配置, 计算 horizonEnd
// endCount = 期数 (与 periodDimension 结合决定实际天数)
ApsTimeConfig timeConfig = apsTimeConfigService.getOne(new LambdaQueryWrapper<>());
ctx.baseTime = (timeConfig != null && timeConfig.getBaseTime() != null)
? timeConfig.getBaseTime() : LocalDateTime.now();
ApsTimeConfig timeConfig = apsTimeConfigService.getOne(
new LambdaQueryWrapper<ApsTimeConfig>()
.eq(ApsTimeConfig::getMpSceneId, "172f545f-f94c-4143-86af-bd65e787e8e6"));
ctx.baseTime = LocalDateTime.of(2026, 9, 28, 0, 0, 0);
ctx.periodDimension = "DAY";
if (timeConfig != null && timeConfig.getPeriodDimension() != null
......
......@@ -73,9 +73,11 @@ public class TestDataBuilder {
* @param skipAutoBuild 如果为 true, 不自动调用 build(), 由子类负责初始化
*/
protected TestDataBuilder(boolean skipAutoBuild) {
if (!skipAutoBuild) {
build();
}
}
public void init() {
build();
}
/** 初始化默认测试数据 (3级BOM, 5产品, 5操作, 4周期) */
......
......@@ -99,14 +99,26 @@ public class MacroPlannerModel {
* 变量创建由 {@link VariableFactory} 负责。
*/
public MacroPlannerModel() {
this.solver = MPSolver.createSolver("SCIP");
this.solver = MPSolver.createSolver("CBC");
if (solver == null) {
throw new RuntimeException("无法加载 SCIP 求解器,请检查 OR-Tools 依赖");
}
solver.enableOutput();
// solver.setSolverSpecificParametersAsString("numerics/feastol = 1e-5");
// solver.setSolverSpecificParametersAsString("numerics/epsilon = 1e-8");
// solver.setSolverSpecificParametersAsString("misc/checkfeastolfac = 1.0");
// solver.setSolverSpecificParametersAsString("presolving/maxrounds = 1");
// solver.setSolverSpecificParametersAsString("crossover = true");
// solver.setSolverSpecificParametersAsString("algorithm = barrier");
solver.setSolverSpecificParametersAsString("log level 2");
// solver.setTimeLimit(5*60*1000);
solver.setNumThreads(4);
solver.enableOutput();
}
// ==================== KPI 变量 setter (由 KpiAggregator 调用) ====================
......
......@@ -422,7 +422,7 @@ public class ResultWriter {
sr.setPeriodStartDate(sd.getPeriod().getStartDate().toString());
sr.setDemandQty(sd.getQuantity());
sr.setPriority(sd.getPriority());
sr.setDemandOrderDate(sd.getDemandOrderDate().toString());
sr.setDemandOrderDate(sd.getDemandOrderDate()==null?"":sd.getDemandOrderDate().toString());
double fulfilled = solutionValue(model.getSalesDemandQtyVars(), sd.getKey()).setScale(3, RoundingMode.HALF_UP).doubleValue();;
double unmet = Math.max(0, sd.getQuantity() - fulfilled);
......@@ -686,7 +686,7 @@ public class ResultWriter {
DemandSummaryResult.PeriodEntry pe = new DemandSummaryResult.PeriodEntry();
pe.periodIndex = sd.getPeriod().getIndex();
pe.periodStartDate = sd.getPeriod().getStartDate().toString();
pe.demandOrderDate= sd.getDemandOrderDate().toString();
pe.demandOrderDate=sd.getDemandOrderDate()==null?"": sd.getDemandOrderDate().toString();
pe.demandQty = demand;
pe.fulfilledQty = fulfilled;
pe.unmetQty = unmet;
......
......@@ -4,6 +4,7 @@ import com.aps.common.util.FileHelper;
import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel;
import com.aps.macroplanner.objective.StrategyLevel;
import com.google.ortools.linearsolver.MPSolver;
import java.util.*;
import java.time.LocalDate;
......@@ -54,10 +55,10 @@ public class SolutionPrinter {
/** 输出完整求解结果 */
public void printAll() {
printBomStructure();
printHeader();
printDailyViews();
printKpiSummary();
// printBomStructure();
// printHeader();
// printDailyViews();
// printKpiSummary();
printStatistics();
}
......@@ -224,8 +225,8 @@ public class SolutionPrinter {
hasProduction = true;
double capacityUsed = ptQty * uo.getCapacityCoeff();
if (up.isUnlimited()) {
writeLog(" %s(%s): %.0f件 (耗时%.1fh)",
op.getName(), formatOutputProducts(op), ptQty, capacityUsed);
writeLog(" %s(%s): %.0f件",
op.getName(), formatOutputProducts(op), ptQty);
} else {
double utilPercent = up.getMaxCapacity() > 0
? capacityUsed / up.getMaxCapacity() * 100 : 0;
......@@ -635,10 +636,17 @@ public class SolutionPrinter {
private void printStatistics() {
long elapsedMs = System.currentTimeMillis() - startTimeMs;
MPSolver solver= model.getSolver();
writeLog("");
writeLog("═══════════════════ 求解统计 ═══════════════════");
writeLog("变量数:%d", model.getSolver().numVariables());
writeLog("约束数:%d", model.getSolver().numConstraints());
writeLog("变量数:%d", solver.numVariables());
writeLog("约束数:%d", solver.numConstraints());
double primalBound = solver.objective().value(); // 当前最好可行解目标值(原始界)
double dualBound = solver.objective().bestBound(); // 全局理论最优边界(对偶界)
writeLog("当前最好可行解目标值:%f", primalBound);
writeLog("全局理论最优边界:%f", dualBound);
double gap = Math.abs(primalBound - dualBound) / Math.abs(primalBound) * 100;
writeLog("gap:%.2f", gap);
writeLog("耗时:%.3f 秒", elapsedMs / 1000.0);
}
......
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