Commit 2521e779 authored by Tong Li's avatar Tong Li

MP

parent d653ce10
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
...@@ -20,4 +20,6 @@ public class ApsTimeConfig { ...@@ -20,4 +20,6 @@ public class ApsTimeConfig {
/** 周期维度: DAY(天) / WEEK(周) / MONTH(月), 默认 DAY */ /** 周期维度: DAY(天) / WEEK(周) / MONTH(月), 默认 DAY */
private String periodDimension; private String periodDimension;
private String MpSceneId;
} }
\ No newline at end of file
package com.aps.macroplanner; 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.google.ortools.Loader;
import com.aps.macroplanner.data.MultiLevelBomTestDataBuilder; import com.aps.macroplanner.data.MultiLevelBomTestDataBuilder;
import com.aps.macroplanner.data.TestDataBuilder; import com.aps.macroplanner.data.TestDataBuilder;
...@@ -23,6 +26,18 @@ import com.aps.macroplanner.data.TestDataBuilder; ...@@ -23,6 +26,18 @@ import com.aps.macroplanner.data.TestDataBuilder;
* </ol> * </ol>
*/ */
public class MultiLevelBomTestRunner { 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) { public static void main(String[] args) {
Loader.loadNativeLibraries(); Loader.loadNativeLibraries();
...@@ -31,12 +46,21 @@ public class MultiLevelBomTestRunner { ...@@ -31,12 +46,21 @@ public class MultiLevelBomTestRunner {
System.out.println("Demand: P1=40/day, P2=30/day"); System.out.println("Demand: P1=40/day, P2=30/day");
System.out.println(); System.out.println();
TestDataBuilder data = new MultiLevelBomTestDataBuilder(); for (int scale : BenchmarkDataBuilder.SUPPORTED_SCALES) {
System.out.println("Data loaded: " + data.getProducts().size() + " products, "
BenchmarkDataBuilder data =
BenchmarkDataBuilder.forScale(scale);
writeLog("===== TEST RUNNER START "+scale+"=====");
data.init();
writeLog("Data loaded: " + data.getProducts().size() + " products, "
+ data.getOperations().size() + " operations"); + data.getOperations().size() + " operations");
MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data); MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
optimizer.buildModel(); optimizer.buildModel();
optimizer.solve(); optimizer.solve(String.valueOf(scale));
System.out.println("===== MULTI-LEVEL BOM TEST RUNNER END ====="); writeLog("===== TEST RUNNER END =====");
}
} }
} }
\ No newline at end of file
...@@ -53,8 +53,8 @@ public class BalanceConstraint { ...@@ -53,8 +53,8 @@ public class BalanceConstraint {
} }
} }
// DemandSlack // DemandSlack
MPVariable slackVar = demandSlackVars.get(invKey); // MPVariable slackVar = demandSlackVars.get(invKey);
if (slackVar != null) balance.setCoefficient(slackVar, 1.0); // if (slackVar != null) balance.setCoefficient(slackVar, 1.0);
// 上一周期库存 (t>0) // 上一周期库存 (t>0)
if (p.getIndex() > 0) { if (p.getIndex() > 0) {
String prevKey = prod.getId() + "_" + sp.getId() + "_" + (p.getIndex() - 1); String prevKey = prod.getId() + "_" + sp.getId() + "_" + (p.getIndex() - 1);
......
package com.aps.macroplanner.constraint; package com.aps.macroplanner.constraint;
import com.aps.common.util.FileHelper;
import com.aps.macroplanner.data.TestDataBuilder; import com.aps.macroplanner.data.TestDataBuilder;
import com.aps.macroplanner.model.MacroPlannerModel; import com.aps.macroplanner.model.MacroPlannerModel;
...@@ -117,7 +118,21 @@ public class ConstraintFactory { ...@@ -117,7 +118,21 @@ public class ConstraintFactory {
logSummary(model); 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。 * 输出新增约束数和新增变量数, 如果为 0 则输出 WARNING。
...@@ -129,7 +144,7 @@ public class ConstraintFactory { ...@@ -129,7 +144,7 @@ public class ConstraintFactory {
int addedVariables = model.getSolver().numVariables() - beforeVariables; int addedVariables = model.getSolver().numVariables() - beforeVariables;
if (addedConstraints == 0 && addedVariables == 0) { if (addedConstraints == 0 && addedVariables == 0) {
LOG.warning(String.format("[%s] %s → 未创建任何约束或变量! (可能数据为空)", writeLog(String.format("[%s] %s → 未创建任何约束或变量! (可能数据为空)",
stepName, description)); stepName, description));
} else { } else {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
...@@ -140,7 +155,7 @@ public class ConstraintFactory { ...@@ -140,7 +155,7 @@ public class ConstraintFactory {
if (addedVariables > 0) { if (addedVariables > 0) {
sb.append(String.format(" | +%d变量", addedVariables)); sb.append(String.format(" | +%d变量", addedVariables));
} }
LOG.info(sb.toString()); writeLog(sb.toString());
} }
} }
......
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);
}
}
...@@ -160,7 +160,7 @@ public class MacroPlannerDataConverter { ...@@ -160,7 +160,7 @@ public class MacroPlannerDataConverter {
// 0. 读取时间配置, 计算 horizonEnd // 0. 读取时间配置, 计算 horizonEnd
// endCount = 期数 (与 periodDimension 结合决定实际天数) // endCount = 期数 (与 periodDimension 结合决定实际天数)
ApsTimeConfig timeConfig = apsTimeConfigService.getOne(new LambdaQueryWrapper<>()); 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.baseTime = LocalDateTime.of(2026,9,28,0,0,0);
// ctx.baseTime = (timeConfig != null && timeConfig.getBaseTime() != null) // ctx.baseTime = (timeConfig != null && timeConfig.getBaseTime() != null)
// ? timeConfig.getBaseTime() : LocalDateTime.now(); // ? timeConfig.getBaseTime() : LocalDateTime.now();
...@@ -188,7 +188,7 @@ public class MacroPlannerDataConverter { ...@@ -188,7 +188,7 @@ public class MacroPlannerDataConverter {
// 1. 需求订单 (ApsDemandOrder, 按 deliverytime 时间范围过滤) // 1. 需求订单 (ApsDemandOrder, 按 deliverytime 时间范围过滤)
// ctx.apsDemandOrders = apsDemandOrderMapper.selectList( // ctx.apsDemandOrders = apsDemandOrderMapper.selectList(
// new LambdaQueryWrapper<ApsDemandOrder>() // new LambdaQueryWrapper<ApsDemandOrder>().eq(ApsDemandOrder::getIsdeleted, 0)
// .ge(ApsDemandOrder::getDeliverytime, ctx.baseTime) // .ge(ApsDemandOrder::getDeliverytime, ctx.baseTime)
// .lt(ApsDemandOrder::getDeliverytime, horizonEndDateTime)); // .lt(ApsDemandOrder::getDeliverytime, horizonEndDateTime));
...@@ -1348,7 +1348,7 @@ public class MacroPlannerDataConverter { ...@@ -1348,7 +1348,7 @@ public class MacroPlannerDataConverter {
String key = m.getProduct().getId() + "_" + m.getStockingPoint().getId() + "_" + p.getIndex(); String key = m.getProduct().getId() + "_" + m.getStockingPoint().getId() + "_" + p.getIndex();
if (invSpecKeys.add(key)) { if (invSpecKeys.add(key)) {
inventorySpecs.add(new InventorySpec(m.getProduct(), m.getStockingPoint(), p, inventorySpecs.add(new InventorySpec(m.getProduct(), m.getStockingPoint(), p,
0.0, 0.0, LOOSE_MAX, false, true, true)); 0.0, 0.0, 0, false, true, false));
} }
} }
} }
...@@ -1361,12 +1361,12 @@ public class MacroPlannerDataConverter { ...@@ -1361,12 +1361,12 @@ public class MacroPlannerDataConverter {
} }
Operation lastOp = ops.get(ops.size() - 1); Operation lastOp = ops.get(ops.size() - 1);
supplySpecs.add(new SupplySpec("Supply-" + r.getId(), supplySpecs.add(new SupplySpec("Supply-" + r.getId(),
0.0, 0.0, LOOSE_MAX, false, Collections.singletonList(lastOp))); 0.0, 0.0, 0, false, Collections.singletonList(lastOp)));
} }
for (Operation op : operations) { for (Operation op : operations) {
if (op.getId().startsWith("OP_PROCURE_")) { if (op.getId().startsWith("OP_PROCURE_")) {
supplySpecs.add(new SupplySpec("Supply-" + op.getId(), supplySpecs.add(new SupplySpec("Supply-" + op.getId(),
0.0, 0.0, LOOSE_MAX, true, Collections.singletonList(op))); 0.0, 0.0, 0, false, Collections.singletonList(op)));
} }
} }
log.info("构建 InventorySpec: {}, SupplySpec: {}", inventorySpecs.size(), supplySpecs.size()); log.info("构建 InventorySpec: {}, SupplySpec: {}", inventorySpecs.size(), supplySpecs.size());
......
...@@ -73,9 +73,11 @@ public class TestDataBuilder { ...@@ -73,9 +73,11 @@ public class TestDataBuilder {
* @param skipAutoBuild 如果为 true, 不自动调用 build(), 由子类负责初始化 * @param skipAutoBuild 如果为 true, 不自动调用 build(), 由子类负责初始化
*/ */
protected TestDataBuilder(boolean skipAutoBuild) { protected TestDataBuilder(boolean skipAutoBuild) {
if (!skipAutoBuild) {
build();
} }
public void init() {
build();
} }
/** 初始化默认测试数据 (3级BOM, 5产品, 5操作, 4周期) */ /** 初始化默认测试数据 (3级BOM, 5产品, 5操作, 4周期) */
......
...@@ -99,14 +99,26 @@ public class MacroPlannerModel { ...@@ -99,14 +99,26 @@ public class MacroPlannerModel {
* 变量创建由 {@link VariableFactory} 负责。 * 变量创建由 {@link VariableFactory} 负责。
*/ */
public MacroPlannerModel() { public MacroPlannerModel() {
this.solver = MPSolver.createSolver("SCIP"); this.solver = MPSolver.createSolver("CBC");
if (solver == null) { if (solver == null) {
throw new RuntimeException("无法加载 SCIP 求解器,请检查 OR-Tools 依赖"); 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 调用) ==================== // ==================== KPI 变量 setter (由 KpiAggregator 调用) ====================
......
...@@ -422,7 +422,7 @@ public class ResultWriter { ...@@ -422,7 +422,7 @@ public class ResultWriter {
sr.setPeriodStartDate(sd.getPeriod().getStartDate().toString()); sr.setPeriodStartDate(sd.getPeriod().getStartDate().toString());
sr.setDemandQty(sd.getQuantity()); sr.setDemandQty(sd.getQuantity());
sr.setPriority(sd.getPriority()); 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 fulfilled = solutionValue(model.getSalesDemandQtyVars(), sd.getKey()).setScale(3, RoundingMode.HALF_UP).doubleValue();;
double unmet = Math.max(0, sd.getQuantity() - fulfilled); double unmet = Math.max(0, sd.getQuantity() - fulfilled);
...@@ -686,7 +686,7 @@ public class ResultWriter { ...@@ -686,7 +686,7 @@ public class ResultWriter {
DemandSummaryResult.PeriodEntry pe = new DemandSummaryResult.PeriodEntry(); DemandSummaryResult.PeriodEntry pe = new DemandSummaryResult.PeriodEntry();
pe.periodIndex = sd.getPeriod().getIndex(); pe.periodIndex = sd.getPeriod().getIndex();
pe.periodStartDate = sd.getPeriod().getStartDate().toString(); pe.periodStartDate = sd.getPeriod().getStartDate().toString();
pe.demandOrderDate= sd.getDemandOrderDate().toString(); pe.demandOrderDate=sd.getDemandOrderDate()==null?"": sd.getDemandOrderDate().toString();
pe.demandQty = demand; pe.demandQty = demand;
pe.fulfilledQty = fulfilled; pe.fulfilledQty = fulfilled;
pe.unmetQty = unmet; pe.unmetQty = unmet;
......
...@@ -4,6 +4,7 @@ import com.aps.common.util.FileHelper; ...@@ -4,6 +4,7 @@ import com.aps.common.util.FileHelper;
import com.aps.macroplanner.data.*; import com.aps.macroplanner.data.*;
import com.aps.macroplanner.model.MacroPlannerModel; import com.aps.macroplanner.model.MacroPlannerModel;
import com.aps.macroplanner.objective.StrategyLevel; import com.aps.macroplanner.objective.StrategyLevel;
import com.google.ortools.linearsolver.MPSolver;
import java.util.*; import java.util.*;
import java.time.LocalDate; import java.time.LocalDate;
...@@ -54,10 +55,10 @@ public class SolutionPrinter { ...@@ -54,10 +55,10 @@ public class SolutionPrinter {
/** 输出完整求解结果 */ /** 输出完整求解结果 */
public void printAll() { public void printAll() {
printBomStructure(); // printBomStructure();
printHeader(); // printHeader();
printDailyViews(); // printDailyViews();
printKpiSummary(); // printKpiSummary();
printStatistics(); printStatistics();
} }
...@@ -224,8 +225,8 @@ public class SolutionPrinter { ...@@ -224,8 +225,8 @@ public class SolutionPrinter {
hasProduction = true; hasProduction = true;
double capacityUsed = ptQty * uo.getCapacityCoeff(); double capacityUsed = ptQty * uo.getCapacityCoeff();
if (up.isUnlimited()) { if (up.isUnlimited()) {
writeLog(" %s(%s): %.0f件 (耗时%.1fh)", writeLog(" %s(%s): %.0f件",
op.getName(), formatOutputProducts(op), ptQty, capacityUsed); op.getName(), formatOutputProducts(op), ptQty);
} else { } else {
double utilPercent = up.getMaxCapacity() > 0 double utilPercent = up.getMaxCapacity() > 0
? capacityUsed / up.getMaxCapacity() * 100 : 0; ? capacityUsed / up.getMaxCapacity() * 100 : 0;
...@@ -635,10 +636,17 @@ public class SolutionPrinter { ...@@ -635,10 +636,17 @@ public class SolutionPrinter {
private void printStatistics() { private void printStatistics() {
long elapsedMs = System.currentTimeMillis() - startTimeMs; long elapsedMs = System.currentTimeMillis() - startTimeMs;
MPSolver solver= model.getSolver();
writeLog(""); writeLog("");
writeLog("═══════════════════ 求解统计 ═══════════════════"); writeLog("═══════════════════ 求解统计 ═══════════════════");
writeLog("变量数:%d", model.getSolver().numVariables()); writeLog("变量数:%d", solver.numVariables());
writeLog("约束数:%d", model.getSolver().numConstraints()); 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); 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