Commit fd3b9e49 authored by Tong Li's avatar Tong Li

MP-将kpi设置接入,优化器

parent 2f9e7b36
...@@ -532,7 +532,8 @@ public class MacroPlannerResultController { ...@@ -532,7 +532,8 @@ public class MacroPlannerResultController {
description = "执行数据库转换→数据验证→MIP建模→分层求解→JSON结果保存的完整流程。" description = "执行数据库转换→数据验证→MIP建模→分层求解→JSON结果保存的完整流程。"
+ "耗时通常数秒到数分钟, 取决于数据规模。") + "耗时通常数秒到数分钟, 取决于数据规模。")
public R<Map<String, Object>> runOptimization( public R<Map<String, Object>> runOptimization(
@RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId) { @RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId,
@RequestParam("kpiSetting") @Parameter(description = "kpi设置", required = true) Integer kpiSetting) {
if (sceneId == null || sceneId.trim().isEmpty()) { if (sceneId == null || sceneId.trim().isEmpty()) {
return R.failed("sceneId不能为空"); return R.failed("sceneId不能为空");
} }
...@@ -543,7 +544,7 @@ public class MacroPlannerResultController { ...@@ -543,7 +544,7 @@ public class MacroPlannerResultController {
try { try {
// 1. 数据转换: 数据库工艺物料类 → macroplanner 实体 // 1. 数据转换: 数据库工艺物料类 → macroplanner 实体
TestDataBuilder data = macroPlannerDataConverter.convert(sid); TestDataBuilder data = macroPlannerDataConverter.convert(sid,kpiSetting);
result.put("dataSummary", buildDataSummary(data)); result.put("dataSummary", buildDataSummary(data));
// 2. 数据验证 // 2. 数据验证
...@@ -560,7 +561,7 @@ public class MacroPlannerResultController { ...@@ -560,7 +561,7 @@ public class MacroPlannerResultController {
long solveStart = System.currentTimeMillis(); long solveStart = System.currentTimeMillis();
MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data); MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
optimizer.buildModel(); optimizer.buildModel();
optimizer.solve(); optimizer.solve(sid);
long solveEnd = System.currentTimeMillis(); long solveEnd = System.currentTimeMillis();
// 4. 保存结果到 JSON 文件 // 4. 保存结果到 JSON 文件
......
...@@ -30,7 +30,7 @@ import org.springframework.context.ApplicationContext; ...@@ -30,7 +30,7 @@ import org.springframework.context.ApplicationContext;
public class MacroPlannerDataConverterRunner { public class MacroPlannerDataConverterRunner {
public static void main(String[] args) { public static void main(String[] args) {
String sceneId = (args.length > 0) ? args[0] : "ff8a2f2b-e134-4ec2-8e2e-b77e830c9e64"; String sceneId = (args.length > 0) ? args[0] : "15b4cf5b-1224-418e-aa6f-1c643ea78d3c";
System.out.println("===== MACROPLANNER DATA CONVERTER RUNNER START ====="); System.out.println("===== MACROPLANNER DATA CONVERTER RUNNER START =====");
System.out.println("SceneId: " + sceneId); System.out.println("SceneId: " + sceneId);
...@@ -40,7 +40,7 @@ public class MacroPlannerDataConverterRunner { ...@@ -40,7 +40,7 @@ public class MacroPlannerDataConverterRunner {
MacroPlannerDataConverter converter = ctx.getBean(MacroPlannerDataConverter.class); MacroPlannerDataConverter converter = ctx.getBean(MacroPlannerDataConverter.class);
// 1. 转换数据: 数据库工艺物料类 → macroplanner 实体 // 1. 转换数据: 数据库工艺物料类 → macroplanner 实体
TestDataBuilder data = converter.convert(sceneId); TestDataBuilder data = converter.convert(sceneId,8);
// 2. 打印转换结果规模 // 2. 打印转换结果规模
System.out.println("Data converted:"); System.out.println("Data converted:");
......
package com.aps.macroplanner; package com.aps.macroplanner;
import com.aps.common.util.FileHelper; import com.aps.common.util.FileHelper;
import com.aps.macroplanner.objective.KpiLib;
import com.aps.macroplanner.objective.KpiSetting;
import com.aps.macroplanner.output.dto.OptimizationResult; import com.aps.macroplanner.output.dto.OptimizationResult;
import com.aps.service.MpPispipResultPersistenceService; import com.aps.service.MpPispipResultPersistenceService;
import com.google.ortools.Loader; import com.google.ortools.Loader;
...@@ -25,9 +27,12 @@ import java.time.LocalDateTime; ...@@ -25,9 +27,12 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.ConsoleHandler; import java.util.logging.ConsoleHandler;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.stream.Collectors;
/** /**
* MacroPlanner 核心优化器 — 供应链产能规划混合整数规划(MIP)模型 * MacroPlanner 核心优化器 — 供应链产能规划混合整数规划(MIP)模型
...@@ -197,11 +202,11 @@ public class MacroPlannerOptimizer { ...@@ -197,11 +202,11 @@ public class MacroPlannerOptimizer {
writeLog(" [OK] 约束与KPI汇总创建完成"); writeLog(" [OK] 约束与KPI汇总创建完成");
// 3. 目标函数 (加权求和) // 3. 目标函数 (加权求和)
ObjectiveBuilder.build(model, data); // ObjectiveBuilder.build(model, data);
writeLog(" [OK] 目标函数创建完成"); writeLog(" [OK] 目标函数创建完成");
// 4. 导出 LP 模型文件 // 4. 导出 LP 模型文件
exportLpModel(); //exportLpModel();
writeLog("\n模型统计: 变量=" + model.getSolver().numVariables() writeLog("\n模型统计: 变量=" + model.getSolver().numVariables()
+ ", 约束=" + model.getSolver().numConstraints() + "\n"); + ", 约束=" + model.getSolver().numConstraints() + "\n");
...@@ -279,12 +284,13 @@ public class MacroPlannerOptimizer { ...@@ -279,12 +284,13 @@ public class MacroPlannerOptimizer {
final long startTimeMs = System.currentTimeMillis(); final long startTimeMs = System.currentTimeMillis();
// ========== 1. 前置参数校验 ========== // ========== 1. 前置参数校验 ==========
KPIWeights w = data.getKpiWeights(); List<KpiSetting> w = data.getKpiSettings();
List<StrategyLevel> levels=new ArrayList<>();
if (w == null) { if (w == null) {
writeLog("[ERROR] KPI权重配置为空,终止求解"); levels = defineLevels(data.getKpiWeights());
return; }else {
levels = getKpiLevels(w);
} }
List<StrategyLevel> levels = defineLevels(w);
if (levels == null || levels.isEmpty()) { if (levels == null || levels.isEmpty()) {
writeLog("[ERROR] 分层策略为空,终止求解"); writeLog("[ERROR] 分层策略为空,终止求解");
return; return;
...@@ -427,31 +433,64 @@ public class MacroPlannerOptimizer { ...@@ -427,31 +433,64 @@ public class MacroPlannerOptimizer {
// === Level 1: 需求满足 (最高优先级, 严格分层, slack=0%) === // === Level 1: 需求满足 (最高优先级, 严格分层, slack=0%) ===
StrategyLevel l1 = new StrategyLevel(1, "需求满足", 0.0); StrategyLevel l1 = new StrategyLevel(1, "需求满足", 0.0);
l1.addKPI("需求缺口", model.getTotalFulfillment(), w.getFulfillmentWeight()); l1.addKPI(KpiLib.Fulfillment.getEn(),KpiLib.Fulfillment.getCn(), model.getTotalFulfillment(), w.getFulfillmentWeight());
levels.add(l1); levels.add(l1);
// === Level 2: 产能约束 (物理硬约束, 严格分层, slack=0%) === // === Level 2: 产能约束 (物理硬约束, 严格分层, slack=0%) ===
StrategyLevel l2 = new StrategyLevel(2, "产能约束", 0.0); StrategyLevel l2 = new StrategyLevel(2, "产能约束", 0.0);
l2.addKPI("产能超载", model.getTotalUnitCapacity(), w.getUnitCapacityWeight()); l2.addKPI(KpiLib.UnitCapacity.getEn(),KpiLib.Fulfillment.getCn(), model.getTotalUnitCapacity(), w.getUnitCapacityWeight());
levels.add(l2); levels.add(l2);
// === Level 3: 业务KPI (允许 5% 退化, slack=5%) === // === Level 3: 业务KPI (允许 5% 退化, slack=5%) ===
StrategyLevel l3 = new StrategyLevel(3, "业务KPI", 0.05); StrategyLevel l3 = new StrategyLevel(3, "业务KPI", 0.05);
l3.addKPI("批次偏差", model.getTotalLotSize(), w.getLotSizeWeight()); l3.addKPI(KpiLib.LotSize.getEn(),KpiLib.LotSize.getCn(), model.getTotalLotSize(), w.getLotSizeWeight());
l3.addKPI("目标库存偏差", model.getTotalTargetInvLevel(), w.getTargetInventoryLevelWeight()); l3.addKPI(KpiLib.TargetInvLevel.getEn(),KpiLib.TargetInvLevel.getCn(), model.getTotalTargetInvLevel(), w.getTargetInventoryLevelWeight());
l3.addKPI("供应目标偏差", model.getTotalSupplyTarget(), w.getSupplyTargetWeight()); l3.addKPI(KpiLib.SupplyTarget.getEn(),KpiLib.SupplyTarget.getCn(), model.getTotalSupplyTarget(), w.getSupplyTargetWeight());
l3.addKPI("销售优先级", model.getTotalSalesDemandPriority(), l3.addKPI(KpiLib.SalesDemandPriority.getEn(),KpiLib.SalesDemandPriority.getCn(), model.getTotalSalesDemandPriority(),
w.getSalesDemandPriorityWeight(), true); // 负系数 = 最大化 w.getSalesDemandPriorityWeight(), true); // 负系数 = 最大化
levels.add(l3); levels.add(l3);
// === Level 4: 软约束 (允许 10% 退化, slack=10%) === // === Level 4: 软约束 (允许 10% 退化, slack=10%) ===
StrategyLevel l4 = new StrategyLevel(4, "软约束", 0.10); StrategyLevel l4 = new StrategyLevel(4, "软约束", 0.10);
l4.addKPI("超库存", model.getTotalMaxInventoryLevel(), w.getMaxInventoryLevelWeight()); l4.addKPI(KpiLib.MaxInventoryLevel.getEn(),KpiLib.MaxInventoryLevel.getCn(), model.getTotalMaxInventoryLevel(), w.getMaxInventoryLevelWeight());
l4.addKPI("欠库存", model.getTotalMinInventoryLevel(), w.getMinInventoryLevelWeight()); l4.addKPI(KpiLib.MinInventoryLevel.getEn(),KpiLib.MinInventoryLevel.getCn(), model.getTotalMinInventoryLevel(), w.getMinInventoryLevelWeight());
l4.addKPI("最小供应不足", model.getTotalMinSupply(), w.getMinSupplyWeight()); l4.addKPI(KpiLib.MinSupply.getEn(),KpiLib.MinSupply.getCn(), model.getTotalMinSupply(), w.getMinSupplyWeight());
l4.addKPI("最大供应超出", model.getTotalMaxSupply(), w.getMaxSupplyWeight()); l4.addKPI(KpiLib.MaxSupply.getEn(),KpiLib.MaxSupply.getCn(), model.getTotalMaxSupply(), w.getMaxSupplyWeight());
levels.add(l4); levels.add(l4);
return levels;
}
private List<StrategyLevel> getKpiLevels(List<KpiSetting> kpis) {
List<StrategyLevel> levels = new ArrayList<>();
Map<Integer, List<KpiSetting>> groupByLevelMap = kpis.stream()
.collect(Collectors.groupingBy(
KpiSetting::getLevel, //如果改成getter
TreeMap::new,
Collectors.toList()
));
for (Map.Entry<Integer, List<KpiSetting>> entry : groupByLevelMap.entrySet()) {
Integer currentLevel = entry.getKey();
List<KpiSetting> kpiGroup = entry.getValue();
StrategyLevel level = new StrategyLevel(currentLevel, "", 0.05);
// 遍历当前分组里面每一个KPI
for (KpiSetting kpi : kpiGroup) {
level.addKPI(kpi.code, kpi.name, model.getKpi(kpi.code), kpi.weight);
}
levels.add(level);
}
return levels; return levels;
} }
......
...@@ -2,10 +2,12 @@ package com.aps.macroplanner.data; ...@@ -2,10 +2,12 @@ package com.aps.macroplanner.data;
import com.aps.common.util.ParamValidator; import com.aps.common.util.ParamValidator;
import com.aps.entity.*; import com.aps.entity.*;
import com.aps.macroplanner.objective.KpiSetting;
import com.aps.macroplanner.scene.MacroSceneContext; import com.aps.macroplanner.scene.MacroSceneContext;
import com.aps.entity.basic.Material; import com.aps.entity.basic.Material;
import com.aps.entity.basic.MaterialSupply; import com.aps.entity.basic.MaterialSupply;
import com.aps.mapper.EquipShiftCapacityMapper; import com.aps.mapper.EquipShiftCapacityMapper;
import com.aps.service.KpiCategoryItemService;
import com.aps.service.PlanPeriodService; import com.aps.service.PlanPeriodService;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
...@@ -131,6 +133,9 @@ public class MacroPlannerDataConverter { ...@@ -131,6 +133,9 @@ public class MacroPlannerDataConverter {
@Autowired @Autowired
private PlanPeriodService planPeriodService; private PlanPeriodService planPeriodService;
@Autowired
private KpiCategoryItemService kpiCategoryItemService;
// ==================== 公开入口 ==================== // ==================== 公开入口 ====================
/** /**
...@@ -139,16 +144,16 @@ public class MacroPlannerDataConverter { ...@@ -139,16 +144,16 @@ public class MacroPlannerDataConverter {
* @param sceneId 场景ID * @param sceneId 场景ID
* @return 填充好的 TestDataBuilder, 可直接传给 MacroPlannerOptimizer * @return 填充好的 TestDataBuilder, 可直接传给 MacroPlannerOptimizer
*/ */
public TestDataBuilder convert(String sceneId) { public TestDataBuilder convert(String sceneId,Integer kpiSetting) {
return MacroSceneContext.execute(sceneId, () -> convertScoped(sceneId)); return MacroSceneContext.execute(sceneId, () -> convertScoped(sceneId, kpiSetting));
} }
private TestDataBuilder convertScoped(String sceneId) { private TestDataBuilder convertScoped(String sceneId,Integer kpiSetting) {
log.info("开始转换场景数据到 macroplanner: sceneId={}", sceneId); log.info("开始转换场景数据到 macroplanner: sceneId={}", sceneId);
ConvertContext ctx = loadRawData(sceneId); ConvertContext ctx = loadRawData(sceneId,kpiSetting);
buildMaterials(ctx); buildMaterials(ctx);
buildDailyCapacityByUnitId(ctx); buildDailyCapacityByUnitId(ctx);
...@@ -167,7 +172,7 @@ public class MacroPlannerDataConverter { ...@@ -167,7 +172,7 @@ public class MacroPlannerDataConverter {
// ==================== 步骤1: 数据加载 ==================== // ==================== 步骤1: 数据加载 ====================
private ConvertContext loadRawData(String sceneId) { private ConvertContext loadRawData(String sceneId,Integer kpiSetting) {
ConvertContext ctx = new ConvertContext(); ConvertContext ctx = new ConvertContext();
// 0. 读取时间配置, 计算 horizonEnd // 0. 读取时间配置, 计算 horizonEnd
...@@ -395,6 +400,14 @@ public class MacroPlannerDataConverter { ...@@ -395,6 +400,14 @@ public class MacroPlannerDataConverter {
} }
log.info("加载设备产能日历(EquipShiftCapacity): {} 条 (时间范围过滤)", ctx.equipShiftCapacities.size()); log.info("加载设备产能日历(EquipShiftCapacity): {} 条 (时间范围过滤)", ctx.equipShiftCapacities.size());
ctx.kpiCategoryItems = kpiCategoryItemService.list(
new LambdaQueryWrapper<KpiCategoryItem>()
.eq(KpiCategoryItem::getIsDeleted, 0)
.eq(KpiCategoryItem::getCategoryId, kpiSetting));
return ctx; return ctx;
} }
...@@ -720,7 +733,7 @@ public class MacroPlannerDataConverter { ...@@ -720,7 +733,7 @@ public class MacroPlannerDataConverter {
fillPeriodsAndUnitPeriods(ctx); fillPeriodsAndUnitPeriods(ctx);
fillSalesDemands(ctx); fillSalesDemands(ctx);
fillInventorySpecsAndSupplySpecs(ctx); fillInventorySpecsAndSupplySpecs(ctx);
fillKpiWeights(); fillKpiWeights(ctx);
} }
// ---------- 步骤3: Product + StockingPoint ---------- // ---------- 步骤3: Product + StockingPoint ----------
...@@ -1499,21 +1512,15 @@ public class MacroPlannerDataConverter { ...@@ -1499,21 +1512,15 @@ public class MacroPlannerDataConverter {
// ---------- 步骤10: KPIWeights (默认值, 复用 RoutingTestDataBuilder) ---------- // ---------- 步骤10: KPIWeights (默认值, 复用 RoutingTestDataBuilder) ----------
private void fillKpiWeights() { private void fillKpiWeights(ConvertContext ctx) {
kpiWeights = new KPIWeights(
100.0, // fulfillmentWeight List<KpiSetting> kpis=new ArrayList<>();
10.0, // lotSizeWeight for (KpiCategoryItem item:ctx.kpiCategoryItems) {
5.0, // maxInventoryLevelWeight KpiSetting kpi=new KpiSetting(item.getKpiName(), item.getKpiCode(),item.getWeight().doubleValue(),item.getSort().intValue());
5.0, // minInventoryLevelWeight kpis.add(kpi);
8.0, // targetInventoryLevelWeight }
3.0, // unitCapacityWeight
8.0, // supplyTargetWeight kpiSettings = kpis;
5.0, // minSupplyWeight
5.0, // maxSupplyWeight
1.0, // salesDemandPriorityWeight
20.0, // postponementPenaltyWeight
5.0 // processMaxQuantityWeight
);
} }
// ---------- 工具方法 ---------- // ---------- 工具方法 ----------
...@@ -1549,6 +1556,11 @@ public class MacroPlannerDataConverter { ...@@ -1549,6 +1556,11 @@ public class MacroPlannerDataConverter {
List<Equipinfo> equipinfos = new ArrayList<>(); List<Equipinfo> equipinfos = new ArrayList<>();
List<PlanResource> planResources = new ArrayList<>(); List<PlanResource> planResources = new ArrayList<>();
List<EquipShiftCapacity> equipShiftCapacities = new ArrayList<>(); List<EquipShiftCapacity> equipShiftCapacities = new ArrayList<>();
List<KpiCategoryItem> kpiCategoryItems = new ArrayList<>();
LocalDateTime baseTime; LocalDateTime baseTime;
/** 计划截止日期 (不包含), 由 baseTime + periodDimension + periodCount 计算 */ /** 计划截止日期 (不包含), 由 baseTime + periodDimension + periodCount 计算 */
LocalDate horizonEnd; LocalDate horizonEnd;
......
package com.aps.macroplanner.data; package com.aps.macroplanner.data;
import com.aps.macroplanner.objective.KpiSetting;
import java.util.*; import java.util.*;
import java.time.LocalDate; import java.time.LocalDate;
...@@ -57,7 +59,12 @@ public class TestDataBuilder { ...@@ -57,7 +59,12 @@ public class TestDataBuilder {
/** 在途供应列表 (供应商已发货, 将在未来周期到货的固定量) */ /** 在途供应列表 (供应商已发货, 将在未来周期到货的固定量) */
protected final List<InTransitSupply> inTransitSupplies = new ArrayList<>(); protected final List<InTransitSupply> inTransitSupplies = new ArrayList<>();
/** KPI 权重 (子类可访问) */ /** KPI 权重 (子类可访问) */
protected KPIWeights kpiWeights;
protected KPIWeights kpiWeights;
protected List<KpiSetting> kpiSettings = new ArrayList<>();
/** 初始库存: key = productId_spId, value = 初始库存量 (子类可访问) */ /** 初始库存: key = productId_spId, value = 初始库存量 (子类可访问) */
/** 初始库存列表 (子类可访问) */ /** 初始库存列表 (子类可访问) */
...@@ -278,21 +285,20 @@ public class TestDataBuilder { ...@@ -278,21 +285,20 @@ public class TestDataBuilder {
supplySpecs.add(new SupplySpec("Supply-R2-Alt", 40.0, 20.0, 80.0, supplySpecs.add(new SupplySpec("Supply-R2-Alt", 40.0, 20.0, 80.0,
false, Arrays.asList( opR2Alt))); false, Arrays.asList( opR2Alt)));
// === KPI 权重 === List<KpiSetting> kpis=new ArrayList<>();
kpiWeights = new KPIWeights( kpis.add(new KpiSetting("需求满足","Fulfillment",100,1));
100.0, // fulfillmentWeight kpis.add(new KpiSetting("产能","UnitCapacity",20,1));
10.0, // lotSizeWeight kpis.add(new KpiSetting("批次","LotSize",10,1));
5.0, // maxInventoryLevelWeight kpis.add(new KpiSetting("目标库存","TargetInvLevel",8,1));
5.0, // minInventoryLevelWeight kpis.add(new KpiSetting("供应目标","SupplyTarget",8,1));
8.0, // targetInventoryLevelWeight kpis.add(new KpiSetting("销售优先级","SalesDemandPriority",1,1));
20.0, // unitCapacityWeight kpis.add(new KpiSetting("最大库存","MaxInventoryLevel",5,1));
8.0, // supplyTargetWeight kpis.add(new KpiSetting("最小库存","MinInventoryLevel",5,1));
5.0, // minSupplyWeight kpis.add(new KpiSetting("最小供应","MinSupply",5,1));
5.0, // maxSupplyWeight kpis.add(new KpiSetting("最大供应","MaxSupply",5,1));
1.0, // salesDemandPriorityWeight kpiSettings=kpis;
20.0, // postponementPenaltyWeight
5.0 // processMaxQuantityWeight
);
} }
// ==================== Getters ==================== // ==================== Getters ====================
...@@ -395,8 +401,11 @@ public class TestDataBuilder { ...@@ -395,8 +401,11 @@ public class TestDataBuilder {
public List<InitialInventory> getInitialInventories() { return initialInventories; } public List<InitialInventory> getInitialInventories() { return initialInventories; }
public List<KpiSetting> getKpiSettings() { return kpiSettings; }
public KPIWeights getKpiWeights() { return kpiWeights; } public KPIWeights getKpiWeights() { return kpiWeights; }
/** /**
* 获取初始库存量 * 获取初始库存量
* @param productId 产品ID * @param productId 产品ID
......
package com.aps.macroplanner.model; package com.aps.macroplanner.model;
import com.aps.macroplanner.objective.KpiLib;
import com.google.ortools.linearsolver.MPSolver; import com.google.ortools.linearsolver.MPSolver;
import com.google.ortools.linearsolver.MPVariable; import com.google.ortools.linearsolver.MPVariable;
...@@ -170,4 +171,46 @@ public class MacroPlannerModel { ...@@ -170,4 +171,46 @@ public class MacroPlannerModel {
public MPVariable getTotalSalesDemandPriority() { return totalSalesDemandPriority; } public MPVariable getTotalSalesDemandPriority() { return totalSalesDemandPriority; }
public MPVariable getTotalPostponementPenalty() { return totalPostponementPenalty; } public MPVariable getTotalPostponementPenalty() { return totalPostponementPenalty; }
public MPVariable getTotalProcessMaxQuantity() { return totalProcessMaxQuantity; } public MPVariable getTotalProcessMaxQuantity() { return totalProcessMaxQuantity; }
public MPVariable getKpi(String code) {
MPVariable kpivar = null;
KpiLib kpi = KpiLib.ofEn(code);
switch (kpi) {
case Fulfillment:
kpivar = totalFulfillment;
break;
case UnitCapacity:
kpivar = totalUnitCapacity;
break;
case LotSize:
kpivar = totalLotSize;
break;
case TargetInvLevel:
kpivar = totalTargetInvLevel;
break;
case SupplyTarget:
kpivar = totalSupplyTarget;
break;
case SalesDemandPriority:
kpivar = totalSalesDemandPriority;
break;
case MaxInventoryLevel:
kpivar = totalMaxInventoryLevel;
break;
case MinInventoryLevel:
kpivar = totalMinInventoryLevel;
break;
case MinSupply:
kpivar = totalMinSupply;
break;
case MaxSupply:
kpivar = totalMaxSupply;
break;
}
return kpivar;
}
} }
\ No newline at end of file
package com.aps.macroplanner.objective;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 作者:佟礼
* 时间:2026-09-22
*/
public enum KpiLib {
Fulfillment("需求满足"),
UnitCapacity("产能"),
LotSize("批次"),
TargetInvLevel("目标库存"),
SupplyTarget("供应目标"),
SalesDemandPriority("销售优先级"),
MaxInventoryLevel("最大库存"),
MinInventoryLevel("最小库存"),
MinSupply("最小供应"),
MaxSupply("最大供应");
private final String en;
private final String cn;
KpiLib(String cn) {
this.en = this.name();
this.cn = cn;
}
public String getEn() {
return en;
}
public String getCn() {
return cn;
}
/**
* 转为 Map<英文编码,中文名称>,用于批量翻译、表头转换
*/
public static Map<String, String> toEnCnMap() {
return Arrays.stream(KpiLib.values())
.collect(Collectors.toMap(KpiLib::getEn, KpiLib::getCn));
}
/**
* 根据英文编码获取枚举,返回null则未匹配
*/
public static KpiLib ofEn(String enName) {
for (KpiLib dict : KpiLib.values()) {
if (dict.getEn().equals(enName)) {
return dict;
}
}
return null;
}
}
package com.aps.macroplanner.objective;
import com.google.ortools.linearsolver.MPVariable;
import lombok.Data;
/**
* 作者:佟礼
* 时间:2026-09-21
*/
@Data
public class KpiSetting {
public final String code;
/**
* 该 KPI 在当前层级内的权重
*/
public final double weight;
public final Integer level;
/**
* KPI 名称
*/
public final String name;
public KpiSetting(String name, String code, double weight, Integer level) {
this.name = name;
this.code = code;
this.weight = weight;
this.level = level;
}
}
...@@ -54,63 +54,63 @@ public class ObjectiveBuilder { ...@@ -54,63 +54,63 @@ public class ObjectiveBuilder {
*/ */
public static void build(MacroPlannerModel model, TestDataBuilder data) { public static void build(MacroPlannerModel model, TestDataBuilder data) {
MPObjective objective = model.getSolver().objective(); MPObjective objective = model.getSolver().objective();
KPIWeights w = data.getKpiWeights(); // KPIWeights w = data.getKpiWeights();
// ===== 惩罚项 (最小化: 正系数) ===== // ===== 惩罚项 (最小化: 正系数) =====
// Fulfillment: 需求松弛惩罚, 权重 100.0 (最高优先级) // Fulfillment: 需求松弛惩罚, 权重 100.0 (最高优先级)
// 含义: 所有周期所有产品的需求松弛总和。值越小表示需求满足越好。 // 含义: 所有周期所有产品的需求松弛总和。值越小表示需求满足越好。
objective.setCoefficient(model.getTotalFulfillment(), w.getFulfillmentWeight()); // objective.setCoefficient(model.getTotalFulfillment(), w.getFulfillmentWeight());
//
// LotSize: 批次偏差惩罚, 权重 10.0 // // LotSize: 批次偏差惩罚, 权重 10.0
// 含义: 所有操作所有周期的批次大小偏差总和。惩罚不按批次生产的量。 // // 含义: 所有操作所有周期的批次大小偏差总和。惩罚不按批次生产的量。
objective.setCoefficient(model.getTotalLotSize(), w.getLotSizeWeight()); // objective.setCoefficient(model.getTotalLotSize(), w.getLotSizeWeight());
//
// MaxInventoryLevel: 超库存惩罚, 权重 5.0 // // MaxInventoryLevel: 超库存惩罚, 权重 5.0
// 含义: 所有超标库存的总和。惩罚库存超过最大水平。 // // 含义: 所有超标库存的总和。惩罚库存超过最大水平。
objective.setCoefficient(model.getTotalMaxInventoryLevel(), w.getMaxInventoryLevelWeight()); // objective.setCoefficient(model.getTotalMaxInventoryLevel(), w.getMaxInventoryLevelWeight());
//
// MinInventoryLevel: 欠库存(安全库存)惩罚, 权重 5.0 // // MinInventoryLevel: 欠库存(安全库存)惩罚, 权重 5.0
// 含义: 所有低于最小库存的总和。惩罚库存低于安全水平。 // // 含义: 所有低于最小库存的总和。惩罚库存低于安全水平。
objective.setCoefficient(model.getTotalMinInventoryLevel(), w.getMinInventoryLevelWeight()); // objective.setCoefficient(model.getTotalMinInventoryLevel(), w.getMinInventoryLevelWeight());
//
// TargetInventoryLevel: 目标库存偏差惩罚, 权重 8.0 // // TargetInventoryLevel: 目标库存偏差惩罚, 权重 8.0
// 含义: 所有低于目标库存的总和。惩罚库存偏离目标水平。 // // 含义: 所有低于目标库存的总和。惩罚库存偏离目标水平。
objective.setCoefficient(model.getTotalTargetInvLevel(), w.getTargetInventoryLevelWeight()); // objective.setCoefficient(model.getTotalTargetInvLevel(), w.getTargetInventoryLevelWeight());
//
// UnitCapacity: 产能超载惩罚, 权重 3.0 // // UnitCapacity: 产能超载惩罚, 权重 3.0
// 含义: 所有设备所有周期的产能超载总和。惩罚超负荷生产。 // // 含义: 所有设备所有周期的产能超载总和。惩罚超负荷生产。
objective.setCoefficient(model.getTotalUnitCapacity(), w.getUnitCapacityWeight()); // objective.setCoefficient(model.getTotalUnitCapacity(), w.getUnitCapacityWeight());
//
// SupplyTarget: 供应目标偏差惩罚, 权重 8.0 // // SupplyTarget: 供应目标偏差惩罚, 权重 8.0
// 含义: 供应低于目标的总缺口。惩罚供应不足。 // // 含义: 供应低于目标的总缺口。惩罚供应不足。
objective.setCoefficient(model.getTotalSupplyTarget(), w.getSupplyTargetWeight()); // objective.setCoefficient(model.getTotalSupplyTarget(), w.getSupplyTargetWeight());
//
// MinSupply: 最小供应不足惩罚, 权重 5.0 // // MinSupply: 最小供应不足惩罚, 权重 5.0
// 含义: 供应低于最小值的总缺口。惩罚供应严重不足。 // // 含义: 供应低于最小值的总缺口。惩罚供应严重不足。
objective.setCoefficient(model.getTotalMinSupply(), w.getMinSupplyWeight()); // objective.setCoefficient(model.getTotalMinSupply(), w.getMinSupplyWeight());
//
// MaxSupply: 最大供应超出惩罚, 权重 5.0 // // MaxSupply: 最大供应超出惩罚, 权重 5.0
// 含义: 供应超出最大值的总量。惩罚过度供应。 // // 含义: 供应超出最大值的总量。惩罚过度供应。
objective.setCoefficient(model.getTotalMaxSupply(), w.getMaxSupplyWeight()); // objective.setCoefficient(model.getTotalMaxSupply(), w.getMaxSupplyWeight());
//
// ===== 收益项 (最大化: 负系数, 因为 OR-Tools 默认最小化) ===== // // ===== 收益项 (最大化: 负系数, 因为 OR-Tools 默认最小化) =====
//
// SalesDemandPriority: 销售需求优先级, 权重 1.0, 负系数 = 最大化 // // SalesDemandPriority: 销售需求优先级, 权重 1.0, 负系数 = 最大化
// 含义: 按优先级加权的销售需求满足量。值越大表示满足的需求越多越好。 // // 含义: 按优先级加权的销售需求满足量。值越大表示满足的需求越多越好。
// 因目标函数是最小化, 此项使用负系数 (最大化 = 加负号后最小化)。 // // 因目标函数是最小化, 此项使用负系数 (最大化 = 加负号后最小化)。
objective.setCoefficient(model.getTotalSalesDemandPriority(), -w.getSalesDemandPriorityWeight()); // objective.setCoefficient(model.getTotalSalesDemandPriority(), -w.getSalesDemandPriorityWeight());
//
// PostponementPenalty: 推迟惩罚, 权重 20.0 (简化版固定为 0) // // PostponementPenalty: 推迟惩罚, 权重 20.0 (简化版固定为 0)
// 含义: 对应原模型中的延迟销售需求惩罚。当前版本未实现推迟逻辑。 // // 含义: 对应原模型中的延迟销售需求惩罚。当前版本未实现推迟逻辑。
objective.setCoefficient(model.getTotalPostponementPenalty(), w.getPostponementPenaltyWeight()); // objective.setCoefficient(model.getTotalPostponementPenalty(), w.getPostponementPenaltyWeight());
//
// ProcessMaxQuantity: 过程最大量惩罚, 权重 5.0 (简化版固定为 0) // // ProcessMaxQuantity: 过程最大量惩罚, 权重 5.0 (简化版固定为 0)
// 含义: 对应原模型中超出最大过程量的惩罚。当前版本未实现。 // // 含义: 对应原模型中超出最大过程量的惩罚。当前版本未实现。
objective.setCoefficient(model.getTotalProcessMaxQuantity(), w.getProcessMaxQuantityWeight()); // objective.setCoefficient(model.getTotalProcessMaxQuantity(), w.getProcessMaxQuantityWeight());
//
// 设置为最小化目标 // // 设置为最小化目标
objective.setMinimization(); // objective.setMinimization();
} }
/** /**
* 计算层级边界约束的上界。 * 计算层级边界约束的上界。
......
...@@ -63,10 +63,14 @@ public class StrategyLevel { ...@@ -63,10 +63,14 @@ public class StrategyLevel {
/** 是否为负向 KPI (越小越好 = 正常惩罚项; false = 正常惩罚项) */ /** 是否为负向 KPI (越小越好 = 正常惩罚项; false = 正常惩罚项) */
public final boolean isNegative; public final boolean isNegative;
/** 编号 */
public final String code;
/** KPI 名称 (用于日志) */ /** KPI 名称 (用于日志) */
public final String name; public final String name;
public KPIEntry(String name, MPVariable variable, double weight, boolean isNegative) { public KPIEntry(String code,String name, MPVariable variable, double weight, boolean isNegative) {
this.code = code;
this.name = name; this.name = name;
this.variable = variable; this.variable = variable;
this.weight = weight; this.weight = weight;
...@@ -102,8 +106,8 @@ public class StrategyLevel { ...@@ -102,8 +106,8 @@ public class StrategyLevel {
* @param variable KPI 汇总变量 * @param variable KPI 汇总变量
* @param weight 权重 (0 = 跳过) * @param weight 权重 (0 = 跳过)
*/ */
public void addKPI(String name, MPVariable variable, double weight) { public void addKPI(String code,String name, MPVariable variable, double weight) {
addKPI(name, variable, weight, false); addKPI(code,name, variable, weight, false);
} }
/** /**
...@@ -114,9 +118,9 @@ public class StrategyLevel { ...@@ -114,9 +118,9 @@ public class StrategyLevel {
* @param weight 权重 (0 = 跳过) * @param weight 权重 (0 = 跳过)
* @param isNegative 是否为负向 KPI (true = 最大化, 使用负系数) * @param isNegative 是否为负向 KPI (true = 最大化, 使用负系数)
*/ */
public void addKPI(String name, MPVariable variable, double weight, boolean isNegative) { public void addKPI(String code, String name, MPVariable variable, double weight, boolean isNegative) {
if (weight > 0.0 && variable != null) { if (weight > 0.0 && variable != null) {
kpis.add(new KPIEntry(name, variable, weight, isNegative)); kpis.add(new KPIEntry(code,name, variable, weight, isNegative));
} }
} }
......
...@@ -535,7 +535,7 @@ public class ResultWriter { ...@@ -535,7 +535,7 @@ public class ResultWriter {
// KPI + 统计 // KPI + 统计
writeLog("Kpi"); writeLog("Kpi");
result.setKpis(buildKpis()); // result.setKpis(buildKpis());
// result.setStatistics(buildStatistics()); // result.setStatistics(buildStatistics());
return result; return result;
......
...@@ -3,6 +3,7 @@ package com.aps.macroplanner.output; ...@@ -3,6 +3,7 @@ package com.aps.macroplanner.output;
import com.aps.common.util.FileHelper; 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.KpiLib;
import com.aps.macroplanner.objective.StrategyLevel; import com.aps.macroplanner.objective.StrategyLevel;
import com.google.ortools.linearsolver.MPSolver; import com.google.ortools.linearsolver.MPSolver;
...@@ -670,7 +671,7 @@ public class SolutionPrinter { ...@@ -670,7 +671,7 @@ public class SolutionPrinter {
double totalWeightedPenalty = 0.0; double totalWeightedPenalty = 0.0;
KPIWeights w = data.getKpiWeights(); KPIWeights w = data.getKpiWeights();
Map<String,Double> weights=new HashMap<>();
for (int i = 0; i < levels.size(); i++) { for (int i = 0; i < levels.size(); i++) {
StrategyLevel level = levels.get(i); StrategyLevel level = levels.get(i);
double objValue = (i < levelObjValues.size()) ? levelObjValues.get(i) : 0.0; double objValue = (i < levelObjValues.size()) ? levelObjValues.get(i) : 0.0;
...@@ -679,6 +680,7 @@ public class SolutionPrinter { ...@@ -679,6 +680,7 @@ public class SolutionPrinter {
List<String> kpiNames = new ArrayList<>(); List<String> kpiNames = new ArrayList<>();
for (StrategyLevel.KPIEntry kpi : level.getKpis()) { for (StrategyLevel.KPIEntry kpi : level.getKpis()) {
kpiNames.add(kpi.name); kpiNames.add(kpi.name);
weights.put(kpi.code,kpi.weight);
} }
writeLog("%-6d %-14s %-12s %-12.2f %s", writeLog("%-6d %-14s %-12s %-12.2f %s",
...@@ -703,39 +705,48 @@ public class SolutionPrinter { ...@@ -703,39 +705,48 @@ public class SolutionPrinter {
double maxSupply = model.getTotalMaxSupply().solutionValue(); double maxSupply = model.getTotalMaxSupply().solutionValue();
double salesPriority = model.getTotalSalesDemandPriority().solutionValue(); double salesPriority = model.getTotalSalesDemandPriority().solutionValue();
double totalPenalty = fulfillment * w.getFulfillmentWeight() double totalPenalty = fulfillment * weights.getOrDefault(KpiLib.Fulfillment.getEn(),0d)
+ lotSize * w.getLotSizeWeight() + lotSize * weights.getOrDefault(KpiLib.LotSize.getEn(),0d)
+ maxInv * w.getMaxInventoryLevelWeight() + maxInv * weights.getOrDefault(KpiLib.MinInventoryLevel.getEn(),0d)
+ minInv * w.getMinInventoryLevelWeight() + minInv * weights.getOrDefault(KpiLib.MinInventoryLevel.getEn(),0d)
+ targetInv * w.getTargetInventoryLevelWeight() + targetInv * weights.getOrDefault(KpiLib.TargetInvLevel.getEn(),0d)
+ capacity * w.getUnitCapacityWeight() + capacity * weights.getOrDefault(KpiLib.UnitCapacity.getEn(),0d)
+ supplyTarget * w.getSupplyTargetWeight() + supplyTarget * weights.getOrDefault(KpiLib.SupplyTarget.getEn(),0d)
+ minSupply * w.getMinSupplyWeight() + minSupply * weights.getOrDefault(KpiLib.MinSupply.getEn(),0d)
+ maxSupply * w.getMaxSupplyWeight() + maxSupply * weights.getOrDefault(KpiLib.MaxSupply.getEn(),0d)
- salesPriority * w.getSalesDemandPriorityWeight(); - salesPriority * weights.getOrDefault(KpiLib.SalesDemandPriority.getEn(),0d);
writeLog(" 加权总惩罚: %.2f", totalPenalty); writeLog(" 加权总惩罚: %.2f", totalPenalty);
writeLog(" 需求缺口: %.2f (权重%.0f × %.2f)",
fulfillment * w.getFulfillmentWeight(), w.getFulfillmentWeight(), fulfillment); for (Map.Entry<String, Double> entry : weights.entrySet()) {
writeLog(" 产能超载: %.2f (权重%.0f × %.2f)", String key = entry.getKey();
capacity * w.getUnitCapacityWeight(), w.getUnitCapacityWeight(), capacity); Double weight = entry.getValue();
writeLog(" 批次偏差: %.2f (权重%.0f × %.2f)", KpiLib kpi = KpiLib.ofEn(key);
lotSize * w.getLotSizeWeight(), w.getLotSizeWeight(), lotSize); double val= model.getKpi(key).solutionValue();
writeLog(" 目标库存偏差: %.2f (权重%.0f × %.2f)", writeLog(" %s: %.2f (权重%.0f × %.2f)",
targetInv * w.getTargetInventoryLevelWeight(), w.getTargetInventoryLevelWeight(), targetInv); kpi.getCn(),val * weight, val, val);
writeLog(" 供应目标偏差: %.2f (权重%.0f × %.2f)", }
supplyTarget * w.getSupplyTargetWeight(), w.getSupplyTargetWeight(), supplyTarget);
writeLog(" 超库存: %.2f (权重%.0f × %.2f)",
maxInv * w.getMaxInventoryLevelWeight(), w.getMaxInventoryLevelWeight(), maxInv); // writeLog(" 产能超载: %.2f (权重%.0f × %.2f)",
writeLog(" 欠库存: %.2f (权重%.0f × %.2f)", // capacity * weights.get(KpiLib.Fulfillment.getEn()), w.getUnitCapacityWeight(), capacity);
minInv * w.getMinInventoryLevelWeight(), w.getMinInventoryLevelWeight(), minInv); // writeLog(" 批次偏差: %.2f (权重%.0f × %.2f)",
writeLog(" 最小供应不足: %.2f (权重%.0f × %.2f)", // lotSize * weights.get(KpiLib.Fulfillment.getEn()), w.getLotSizeWeight(), lotSize);
minSupply * w.getMinSupplyWeight(), w.getMinSupplyWeight(), minSupply); // writeLog(" 目标库存偏差: %.2f (权重%.0f × %.2f)",
writeLog(" 最大供应超出: %.2f (权重%.0f × %.2f)", // targetInv * w.getTargetInventoryLevelWeight(), w.getTargetInventoryLevelWeight(), targetInv);
maxSupply * w.getMaxSupplyWeight(), w.getMaxSupplyWeight(), maxSupply); // writeLog(" 供应目标偏差: %.2f (权重%.0f × %.2f)",
writeLog(" 销售优先级(负): %.2f (权重%.0f × %.2f)", // supplyTarget * w.getSupplyTargetWeight(), w.getSupplyTargetWeight(), supplyTarget);
salesPriority * w.getSalesDemandPriorityWeight(), // writeLog(" 超库存: %.2f (权重%.0f × %.2f)",
w.getSalesDemandPriorityWeight(), salesPriority); // maxInv * w.getMaxInventoryLevelWeight(), w.getMaxInventoryLevelWeight(), maxInv);
// writeLog(" 欠库存: %.2f (权重%.0f × %.2f)",
// minInv * w.getMinInventoryLevelWeight(), w.getMinInventoryLevelWeight(), minInv);
// writeLog(" 最小供应不足: %.2f (权重%.0f × %.2f)",
// minSupply * w.getMinSupplyWeight(), w.getMinSupplyWeight(), minSupply);
// writeLog(" 最大供应超出: %.2f (权重%.0f × %.2f)",
// maxSupply * w.getMaxSupplyWeight(), w.getMaxSupplyWeight(), maxSupply);
// writeLog(" 销售优先级(负): %.2f (权重%.0f × %.2f)",
// salesPriority * w.getSalesDemandPriorityWeight(),
// w.getSalesDemandPriorityWeight(), salesPriority);
} }
// ==================== 辅助方法 ==================== // ==================== 辅助方法 ====================
......
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