Commit b70a4972 authored by Tong Li's avatar Tong Li

Merge remote-tracking branch 'origin/tl'

parents 0babbc59 2521e779
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, "
+ data.getOperations().size() + " operations"); BenchmarkDataBuilder data =
MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data); BenchmarkDataBuilder.forScale(scale);
optimizer.buildModel(); writeLog("===== TEST RUNNER START "+scale+"=====");
optimizer.solve();
System.out.println("===== MULTI-LEVEL BOM TEST RUNNER END ====="); 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 { ...@@ -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