Commit a8a8f6ea authored by Tong Li's avatar Tong Li

MP-调整目标值为最大化,增加kpi 总需求松弛, 增加固定第0层,包含kpi总需求松弛

parent 73c9b8d7
......@@ -1024,7 +1024,7 @@ public class MacroPlannerResultController {
entryMap.put("name", e.name);
entryMap.put("rawValue", String.format("%.2f", e.rawValue));
entryMap.put("weight", String.format("%.1f", e.weight));
entryMap.put("penalty", String.format("%.2f", e.penalty));
entryMap.put("contribution", String.format("%.2f", e.contribution));
kpiList.add(entryMap);
}
kpiData.put("entries", kpiList);
......
......@@ -286,7 +286,7 @@ public class MacroPlannerOptimizer {
// ========== 1. 前置参数校验 ==========
List<KpiSetting> w = data.getKpiSettings();
List<StrategyLevel> levels=new ArrayList<>();
if (w == null) {
if (w == null||w.size()==0) {
levels = defineLevels(data.getKpiWeights());
}else {
levels = getKpiLevels(w);
......@@ -342,16 +342,18 @@ public class MacroPlannerOptimizer {
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(" Goal Score : %+.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);
double contribution = kpi.effectiveCoefficient() * kpiValue;
System.out.printf(" %s: %.2f (系数=%+.1f, 得分贡献=%+.2f)%n",
kpi.name, kpiValue, kpi.effectiveCoefficient(), contribution);
}
levelResults.add(new LevelResult(level, optimalValue, status));
......@@ -431,14 +433,19 @@ public class MacroPlannerOptimizer {
private List<StrategyLevel> defineLevels(KPIWeights w) {
List<StrategyLevel> levels = new ArrayList<>();
// === Level 0: 需求满足 (最高优先级, 严格分层, slack=0%) ===
StrategyLevel l0 = new StrategyLevel(0, "需求松弛", 0.0);
l0.addKPI("TotalSlack","需求松弛", model.getTotalSlack(), 1);
levels.add(l0);
// === Level 1: 需求满足 (最高优先级, 严格分层, slack=0%) ===
StrategyLevel l1 = new StrategyLevel(1, "需求满足", 0.0);
l1.addKPI(KpiLib.Fulfillment.getEn(),KpiLib.Fulfillment.getCn(), model.getTotalFulfillment(), w.getFulfillmentWeight());
l1.addKPI(KpiLib.Fulfillment.getEn(),KpiLib.Fulfillment.getCn(), model.getTotalFulfillment(), w.getFulfillmentWeight(),true);
levels.add(l1);
// === Level 2: 产能约束 (物理硬约束, 严格分层, slack=0%) ===
StrategyLevel l2 = new StrategyLevel(2, "产能约束", 0.0);
l2.addKPI(KpiLib.UnitCapacity.getEn(),KpiLib.Fulfillment.getCn(), model.getTotalUnitCapacity(), w.getUnitCapacityWeight());
l2.addKPI(KpiLib.UnitCapacity.getEn(),KpiLib.UnitCapacity.getCn(), model.getTotalUnitCapacity(), w.getUnitCapacityWeight());
levels.add(l2);
// === Level 3: 业务KPI (允许 5% 退化, slack=5%) ===
......@@ -472,7 +479,10 @@ public class MacroPlannerOptimizer {
TreeMap::new,
Collectors.toList()
));
// === Level 0: 需求满足 (最高优先级, 严格分层, slack=0%) ===
StrategyLevel l0 = new StrategyLevel(0, "需求松弛", 0.0);
l0.addKPI("TotalSlack","需求松弛", model.getTotalSlack(), 1);
levels.add(l0);
for (Map.Entry<Integer, List<KpiSetting>> entry : groupByLevelMap.entrySet()) {
Integer currentLevel = entry.getKey();
List<KpiSetting> kpiGroup = entry.getValue();
......@@ -480,7 +490,7 @@ public class MacroPlannerOptimizer {
// 遍历当前分组里面每一个KPI
for (KpiSetting kpi : kpiGroup) {
level.addKPI(kpi.code, kpi.name, model.getKpi(kpi.code), kpi.weight);
level.addKPI(kpi.code, kpi.name, model.getKpi(kpi.code), kpi.weight,KpiLib.ofEn(kpi.code).getIsBenefit());
}
......
......@@ -38,8 +38,12 @@ public class KpiAggregator {
public static void build(MacroPlannerModel model, TestDataBuilder data) {
double inf = MPSolver.infinity();
// TotalFulfillment = Σ DemandSlack
// TotalFulfillment = Σ SalesDemandQty (Quintiq fulfillment bonus KPI)
model.setTotalFulfillment(createSumKpi(model, "TotalFulfillment",
model.getSalesDemandQtyVars()));
// TotalSlack = Σ DemandSlack (Quintiq Slack KPI, 高优先级惩罚)
model.setTotalSlack(createSumKpi(model, "TotalSlack",
model.getDemandSlackVars()));
// TotalLotSize = Σ PTLotSizeOver + Σ PTLotSizeUnder
......@@ -66,10 +70,16 @@ public class KpiAggregator {
model.setTotalUnitCapacity(createSumKpi(model, "TotalUnitCapacity",
model.getCapacityOverloadedVars()));
// TotalMinimumUnitCapacity = Σ CapacityNotMet (Quintiq 最小产能未满足 KPI)
model.setTotalMinimumUnitCapacity(createSumKpi(model, "TotalMinUnitCapacity",
model.getCapacityNotMetVars()));
// TotalSupplyTarget = Σ SupplyTargetQtyUnder
model.setTotalSupplyTarget(createSumKpi(model, "TotalSupplyTarget",
model.getSupplyTargetQtyUnderVars()));
// TotalMinSupply = Σ MinSupplyQtyUnder
model.setTotalMinSupply(createSumKpi(model, "TotalMinSupply",
model.getMinSupplyQtyUnderVars()));
......
......@@ -80,12 +80,19 @@ public class MacroPlannerModel {
private final Map<String, MPVariable> ptLotSizeUnderVars = new HashMap<>();
// ==================== KPI 汇总变量 ====================
private MPVariable totalSlack;
private MPVariable totalFulfillment;
private MPVariable totalLotSize;
private MPVariable totalMaxInventoryLevel;
private MPVariable totalMinInventoryLevel;
private MPVariable totalTargetInvLevel;
private MPVariable totalUnitCapacity;
private MPVariable totalMinimumUnitCapacity;
private MPVariable totalSupplyTarget;
private MPVariable totalMinSupply;
private MPVariable totalMaxSupply;
......@@ -123,6 +130,7 @@ public class MacroPlannerModel {
}
// ==================== KPI 变量 setter (由 KpiAggregator 调用) ====================
public void setTotalSlack(MPVariable v) { this.totalSlack = v; }
public void setTotalFulfillment(MPVariable v) { this.totalFulfillment = v; }
public void setTotalLotSize(MPVariable v) { this.totalLotSize = v; }
......@@ -130,6 +138,9 @@ public class MacroPlannerModel {
public void setTotalMinInventoryLevel(MPVariable v) { this.totalMinInventoryLevel = v; }
public void setTotalTargetInvLevel(MPVariable v) { this.totalTargetInvLevel = v; }
public void setTotalUnitCapacity(MPVariable v) { this.totalUnitCapacity = v; }
public void setTotalMinimumUnitCapacity(MPVariable v) { this.totalMinimumUnitCapacity = v; }
public void setTotalSupplyTarget(MPVariable v) { this.totalSupplyTarget = v; }
public void setTotalMinSupply(MPVariable v) { this.totalMinSupply = v; }
public void setTotalMaxSupply(MPVariable v) { this.totalMaxSupply = v; }
......@@ -150,6 +161,8 @@ public class MacroPlannerModel {
public Map<String, MPVariable> getDemandFulfillmentVars() { return demandFulfillmentVars; }
public Map<String, MPVariable> getCapacityOverloadedVars() { return capacityOverloadedVars; }
public Map<String, MPVariable> getCapacityNotMetVars() { return capacityNotMetVars; }
public Map<String, MPVariable> getMinInvQtyUnderVars() { return minInvQtyUnderVars; }
public Map<String, MPVariable> getMaxInvQtyOverVars() { return maxInvQtyOverVars; }
public Map<String, MPVariable> getInvQtyUnderTargetVars() { return invQtyUnderTargetVars; }
......@@ -159,12 +172,16 @@ public class MacroPlannerModel {
public Map<String, MPVariable> getPtLotSizeOverVars() { return ptLotSizeOverVars; }
public Map<String, MPVariable> getPtLotSizeUnderVars() { return ptLotSizeUnderVars; }
public MPVariable getTotalSlack() { return totalSlack; }
public MPVariable getTotalFulfillment() { return totalFulfillment; }
public MPVariable getTotalLotSize() { return totalLotSize; }
public MPVariable getTotalMaxInventoryLevel() { return totalMaxInventoryLevel; }
public MPVariable getTotalMinInventoryLevel() { return totalMinInventoryLevel; }
public MPVariable getTotalTargetInvLevel() { return totalTargetInvLevel; }
public MPVariable getTotalUnitCapacity() { return totalUnitCapacity; }
public MPVariable getTotalMinimumUnitCapacity() { return totalMinimumUnitCapacity; }
public MPVariable getTotalSupplyTarget() { return totalSupplyTarget; }
public MPVariable getTotalMinSupply() { return totalMinSupply; }
public MPVariable getTotalMaxSupply() { return totalMaxSupply; }
......@@ -183,6 +200,9 @@ public class MacroPlannerModel {
case UnitCapacity:
kpivar = totalUnitCapacity;
break;
case MinUnitCapacity:
kpivar = totalMinimumUnitCapacity;
break;
case LotSize:
kpivar = totalLotSize;
break;
......
......@@ -9,23 +9,28 @@ import java.util.stream.Collectors;
* 时间:2026-09-22
*/
public enum KpiLib {
Fulfillment("需求满足"),
UnitCapacity("产能"),
LotSize("批次"),
TargetInvLevel("目标库存"),
SupplyTarget("供应目标"),
SalesDemandPriority("销售优先级"),
MaxInventoryLevel("最大库存"),
MinInventoryLevel("最小库存"),
MinSupply("最小供应"),
MaxSupply("最大供应");
Fulfillment("需求满足",true),
UnitCapacity("产能",false),
MinUnitCapacity("最小化产能",false),
LotSize("批次",false),
TargetInvLevel("目标库存",false),
SupplyTarget("供应目标",false),
SalesDemandPriority("销售优先级",false),
MaxInventoryLevel("最大库存",false),
MinInventoryLevel("最小库存",false),
MinSupply("最小供应",false),
MaxSupply("最大供应",false);
private final String en;
private final String cn;
KpiLib(String cn) {
/** 是否为收益 KPI(越大越好)。 */
public final boolean isBenefit;
KpiLib(String cn,boolean isBenefit) {
this.en = this.name();
this.cn = cn;
this.isBenefit=isBenefit;
}
public String getEn() {
......@@ -36,6 +41,10 @@ public enum KpiLib {
return cn;
}
public boolean getIsBenefit() {
return isBenefit;
}
/**
* 转为 Map<英文编码,中文名称>,用于批量翻译、表头转换
*/
......
......@@ -168,18 +168,18 @@ public class ObjectiveBuilder {
for (StrategyLevel.KPIEntry kpi : level.getKpis()) {
objective.setCoefficient(kpi.variable, kpi.effectiveCoefficient());
}
objective.setMinimization();
objective.setMaximization();
}
/**
* 添加层级边界约束 — 限制上层目标值不超过最优值 × (1 + slack)。
* 添加层级边界约束 — 限制上层得分不低于最优值减去允许松弛。
*
* <p>该约束确保在求解下层 KPI 时, 上层 KPI 不会退化超过允许范围。
* 对应 Quintiq 中 StrategyLevel 的 HierarchicalSolver 约束。</p>
*
* <h3>数学公式</h3>
* <pre>
* Σ (effectiveCoeff × KPI_variable) ≤ optimalValue + |optimalValue| × relativeGoalSlack
* Σ (effectiveCoeff × KPI_variable) ≥ optimalValue - |optimalValue × relativeGoalSlack|
* </pre>
*
* @param model 模型容器
......@@ -192,10 +192,10 @@ public class ObjectiveBuilder {
if (level.getRelativeGoalSlack() < 0.0) return; // 负松弛表示不约束
MPSolver solver = model.getSolver();
double upperBound = computeUpperBound(optimalValue, level.getRelativeGoalSlack());
double lowerBound = optimalValue - Math.abs(optimalValue * level.getRelativeGoalSlack());
MPConstraint bound = solver.makeConstraint(
-MPSolver.infinity(), upperBound,
lowerBound, MPSolver.infinity(),
"HierLevel" + level.getLevel() + "_Bound");
for (StrategyLevel.KPIEntry kpi : level.getKpis()) {
......
......@@ -60,8 +60,8 @@ public class StrategyLevel {
/** 该 KPI 在当前层级内的权重 */
public final double weight;
/** 是否为负向 KPI (越小越好 = 正常惩罚项; false = 正常惩罚项) */
public final boolean isNegative;
/** 是否为收益 KPI(越大越好)。 */
public final boolean isBenefit;
/** 编号 */
public final String code;
......@@ -69,20 +69,20 @@ public class StrategyLevel {
/** KPI 名称 (用于日志) */
public final String name;
public KPIEntry(String code,String name, MPVariable variable, double weight, boolean isNegative) {
public KPIEntry(String code,String name, MPVariable variable, double weight, boolean isBenefit) {
this.code = code;
this.name = name;
this.variable = variable;
this.weight = weight;
this.isNegative = isNegative;
this.isBenefit = isBenefit;
}
/**
* 计算该 KPI 在目标函数中的实际系数。
* 负向 KPI (如 SalesDemandPriority) 使用负系数实现最大化。
* 收益 KPI 使用正系数;惩罚 KPI 使用负系数。
*/
public double effectiveCoefficient() {
return isNegative ? -weight : weight;
return isBenefit ? weight : -weight;
}
}
......@@ -116,11 +116,11 @@ public class StrategyLevel {
* @param name KPI 名称
* @param variable KPI 汇总变量
* @param weight 权重 (0 = 跳过)
* @param isNegative 是否为负向 KPI (true = 最大化, 使用负系数)
* @param isBenefit 是否为收益 KPI(true = 最大化得分中的正系数)
*/
public void addKPI(String code, String name, MPVariable variable, double weight, boolean isNegative) {
public void addKPI(String code,String name, MPVariable variable, double weight, boolean isBenefit) {
if (weight > 0.0 && variable != null) {
kpis.add(new KPIEntry(code,name, variable, weight, isNegative));
kpis.add(new KPIEntry(code,name, variable, weight, isBenefit));
}
}
......
......@@ -535,7 +535,7 @@ public class ResultWriter {
// KPI + 统计
writeLog("Kpi");
// result.setKpis(buildKpis());
result.setKpis(buildKpis());
// result.setStatistics(buildStatistics());
return result;
......@@ -1491,6 +1491,8 @@ public class ResultWriter {
double objValue = model.getSolver().objective().value();
kr.setObjectiveValue(objValue);
kr.addEntry("需求缺口(Fulfillment)",
model.getTotalFulfillment().solutionValue(),
w.getFulfillmentWeight(), false);
......@@ -1715,7 +1717,7 @@ public class ResultWriter {
.key("name").val(e.name)
.key("rawValue").val(e.rawValue)
.key("weight").val(e.weight)
.key("penalty").val(e.penalty)
.key("contribution").val(e.contribution)
.key("isBenefit").val(e.isBenefit)
.endObj();
}
......
......@@ -693,37 +693,37 @@ public class SolutionPrinter {
// 计算并输出加权总惩罚 (等价于单目标函数值)
writeLog("");
writeLog("--- 最终 KPI 值 (加权总惩罚) ---");
double fulfillment = model.getTotalFulfillment().solutionValue();
double lotSize = model.getTotalLotSize().solutionValue();
double maxInv = model.getTotalMaxInventoryLevel().solutionValue();
double minInv = model.getTotalMinInventoryLevel().solutionValue();
double targetInv = model.getTotalTargetInvLevel().solutionValue();
double capacity = model.getTotalUnitCapacity().solutionValue();
double supplyTarget = model.getTotalSupplyTarget().solutionValue();
double minSupply = model.getTotalMinSupply().solutionValue();
double maxSupply = model.getTotalMaxSupply().solutionValue();
double salesPriority = model.getTotalSalesDemandPriority().solutionValue();
double totalPenalty = fulfillment * weights.getOrDefault(KpiLib.Fulfillment.getEn(),0d)
+ lotSize * weights.getOrDefault(KpiLib.LotSize.getEn(),0d)
+ maxInv * weights.getOrDefault(KpiLib.MinInventoryLevel.getEn(),0d)
+ minInv * weights.getOrDefault(KpiLib.MinInventoryLevel.getEn(),0d)
+ targetInv * weights.getOrDefault(KpiLib.TargetInvLevel.getEn(),0d)
+ capacity * weights.getOrDefault(KpiLib.UnitCapacity.getEn(),0d)
+ supplyTarget * weights.getOrDefault(KpiLib.SupplyTarget.getEn(),0d)
+ minSupply * weights.getOrDefault(KpiLib.MinSupply.getEn(),0d)
+ maxSupply * weights.getOrDefault(KpiLib.MaxSupply.getEn(),0d)
- salesPriority * weights.getOrDefault(KpiLib.SalesDemandPriority.getEn(),0d);
writeLog(" 加权总惩罚: %.2f", totalPenalty);
// writeLog("--- 最终 KPI 值 (加权总惩罚) ---");
// double fulfillment = model.getTotalFulfillment().solutionValue();
// double lotSize = model.getTotalLotSize().solutionValue();
// double maxInv = model.getTotalMaxInventoryLevel().solutionValue();
// double minInv = model.getTotalMinInventoryLevel().solutionValue();
// double targetInv = model.getTotalTargetInvLevel().solutionValue();
// double capacity = model.getTotalUnitCapacity().solutionValue();
// double supplyTarget = model.getTotalSupplyTarget().solutionValue();
// double minSupply = model.getTotalMinSupply().solutionValue();
// double maxSupply = model.getTotalMaxSupply().solutionValue();
// double salesPriority = model.getTotalSalesDemandPriority().solutionValue();
// double totalPenalty = fulfillment * weights.getOrDefault(KpiLib.Fulfillment.getEn(),0d)
// + lotSize * weights.getOrDefault(KpiLib.LotSize.getEn(),0d)
// + maxInv * weights.getOrDefault(KpiLib.MinInventoryLevel.getEn(),0d)
// + minInv * weights.getOrDefault(KpiLib.MinInventoryLevel.getEn(),0d)
// + targetInv * weights.getOrDefault(KpiLib.TargetInvLevel.getEn(),0d)
// + capacity * weights.getOrDefault(KpiLib.UnitCapacity.getEn(),0d)
// + supplyTarget * weights.getOrDefault(KpiLib.SupplyTarget.getEn(),0d)
// + minSupply * weights.getOrDefault(KpiLib.MinSupply.getEn(),0d)
// + maxSupply * weights.getOrDefault(KpiLib.MaxSupply.getEn(),0d)
// - salesPriority * weights.getOrDefault(KpiLib.SalesDemandPriority.getEn(),0d);
//
// writeLog(" 总kpi: %.2f", totalPenalty);
for (Map.Entry<String, Double> entry : weights.entrySet()) {
String key = entry.getKey();
Double weight = entry.getValue();
KpiLib kpi = KpiLib.ofEn(key);
double val= model.getKpi(key).solutionValue();
writeLog(" %s: %.2f (权重%.0f × %.2f)",
writeLog(" %s: %.2f (%.0f × %.2f)",
kpi.getCn(),val * weight, val, weight);
}
......
......@@ -24,8 +24,8 @@ public class KpiResult {
public double rawValue;
/** 权重 */
public double weight;
/** 加权惩罚 = rawValue × weight */
public double penalty;
/** 得分贡献 = rawValue × weight(收益项为正, 惩罚项为负) */
public double contribution;
/** 是否为收益项 (越大越好, 正系数) */
public boolean isBenefit;
}
......@@ -43,7 +43,7 @@ public class KpiResult {
e.rawValue = rawValue;
e.weight = weight;
e.isBenefit = isBenefit;
e.penalty = isBenefit ? -rawValue * weight : rawValue * weight;
e.contribution = isBenefit ? rawValue * weight : -rawValue * weight;
entries.add(e);
}
}
\ No newline at end of file
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