Commit 18808dc2 authored by DESKTOP-VKRD9QF\Administration's avatar DESKTOP-VKRD9QF\Administration

Merge origin/master and preserve KPI output changes

parents a61e73b3 9e6f7171
...@@ -165,12 +165,30 @@ public class MacroPlannerResultController { ...@@ -165,12 +165,30 @@ public class MacroPlannerResultController {
.skip((long)(pageNumber-1)*pageSize) .skip((long)(pageNumber-1)*pageSize)
.limit(pageSize) .limit(pageSize)
.collect(Collectors.toList()); .collect(Collectors.toList());
if(SalesDemandSummaries!=null&&SalesDemandSummaries.size()>0)
{
List<String> SalesDemandids=SalesDemandSummaries.stream()
.map(SalesDemandResult::getSalesDemandId)
.collect(Collectors.toList());
List<RiskItem> Risks= getSalesDemandReason(sceneId,SalesDemandids);
if(Risks!=null&&Risks.size()>0) {
for (SalesDemandResult entry : SalesDemandSummaries) {
List<RiskItem> Risks1= Risks.stream()
.filter(t->t.getSalesDemandId().equals(entry.getSalesDemandId())&&t.getType()==1)
.collect(Collectors.toList());
entry.setUnmetReasons(Risks1);
List<RiskItem> Risks2= Risks.stream()
.filter(t->t.getSalesDemandId().equals(entry.getSalesDemandId())&&t.getType()==2)
.collect(Collectors.toList());
entry.setRisks(Risks2);
}
}
}
// List<Map<String, Object>> summaries = new ArrayList<>();
// for (SalesDemandResult entry : SalesDemandSummaries) {
//
// }
double totalDemand = lists.stream().mapToDouble(SalesDemandResult::getDemandQty).sum(); double totalDemand = lists.stream().mapToDouble(SalesDemandResult::getDemandQty).sum();
double totalFulfilled = lists.stream().mapToDouble(SalesDemandResult::getFulfilledQty).sum(); double totalFulfilled = lists.stream().mapToDouble(SalesDemandResult::getFulfilledQty).sum();
...@@ -188,32 +206,19 @@ public class MacroPlannerResultController { ...@@ -188,32 +206,19 @@ public class MacroPlannerResultController {
return null; return null;
} }
} }
private List<SalesDemandResult> getSalesDemandsResult(String sceneId,String salesDemandId) { private List<RiskItem> getSalesDemandReason(String sceneId,List<String> salesDemandIds) {
try { try {
FlatParquetUtil parquetUtil = new FlatParquetUtil(); FlatParquetUtil parquetUtil = new FlatParquetUtil();
ResultWriter rw = new ResultWriter(); ResultWriter rw = new ResultWriter();
List<FlatParquetUtil.FilterCondition<?>> filters = new ArrayList<>();
filters.add(FlatParquetUtil.FilterCondition.eq("salesDemandId", salesDemandId));
List<FlatParquetUtil.SortCondition> sorts = Arrays.asList( String salesDemandReasonPath = rw.getOptimizationSalesDemandReasons(sceneId);
FlatParquetUtil.SortCondition.asc("periodIndex")
); List<RiskItem> list= parquetUtil.readAll(salesDemandReasonPath,RiskItem.class,oi -> salesDemandIds.contains(oi.getSalesDemandId()));
FlatParquetUtil.QueryRequest request =
new FlatParquetUtil.QueryRequest(
FlatParquetUtil.PageRequest.of(1, 500),
filters, return list;
sorts
);
FlatParquetUtil.PageResult<SalesDemandResult> result =
parquetUtil.queryPage(
rw.getOptimizationSalesDemand(sceneId),
SalesDemandResult.class,
request
);
List<SalesDemandResult> pispips = result.getRecords();
return pispips;
} catch (Exception e) { } catch (Exception e) {
return null; return null;
} }
...@@ -678,25 +683,7 @@ public class MacroPlannerResultController { ...@@ -678,25 +683,7 @@ public class MacroPlannerResultController {
} }
} }
private void normalizeUnmetReasons(List<SalesDemandResult> demands) {
for (SalesDemandResult demand : demands) {
List<String> reasons = demand.getUnmetReasons();
if (reasons == null || reasons.isEmpty()) {
continue;
}
List<String> normalized = new ArrayList<>();
for (String reason : reasons) {
if (reason != null && reason.startsWith("库存耗尽(") && reason.endsWith(")")) {
normalized.add("库存耗尽");
normalized.add(reason.substring("库存耗尽".length()));
} else {
normalized.add(reason);
}
}
reasons.clear();
reasons.addAll(normalized);
}
}
private void enrichPispipProductCodes(List<PispipResult> pispips) { private void enrichPispipProductCodes(List<PispipResult> pispips) {
if (pispips == null || pispips.isEmpty()) { if (pispips == null || pispips.isEmpty()) {
...@@ -1025,6 +1012,7 @@ public class MacroPlannerResultController { ...@@ -1025,6 +1012,7 @@ public class MacroPlannerResultController {
entryMap.put("rawValue", String.format("%.2f", e.rawValue)); entryMap.put("rawValue", String.format("%.2f", e.rawValue));
entryMap.put("weight", e.weight == null ? null : String.format("%.1f", e.weight)); entryMap.put("weight", e.weight == null ? null : String.format("%.1f", e.weight));
entryMap.put("penalty", e.penalty == null ? null : String.format("%.2f", e.penalty)); entryMap.put("penalty", e.penalty == null ? null : String.format("%.2f", e.penalty));
entryMap.put("contribution", e.contribution == null ? null : String.format("%.2f", e.contribution));
kpiList.add(entryMap); kpiList.add(entryMap);
} }
kpiData.put("entries", kpiList); kpiData.put("entries", kpiList);
......
...@@ -80,8 +80,8 @@ public class MacroPlannerDataConverterRunner { ...@@ -80,8 +80,8 @@ public class MacroPlannerDataConverterRunner {
optimizer.solve(); optimizer.solve();
ResultWriter writer = new ResultWriter(optimizer.getModel(), optimizer.getData(), 1); ResultWriter writer = new ResultWriter(optimizer.getModel(), optimizer.getData(), 1);
OptimizationResult result = writer.buildResult("Default"); OptimizationResult result = writer.buildResult(sceneId);
boolean jsonPath = writer.saveResultToFile("Default", result); boolean jsonPath = writer.saveResultToFile(sceneId, result);
System.out.println("===== MACROPLANNER DATA CONVERTER RUNNER END ====="); System.out.println("===== MACROPLANNER DATA CONVERTER RUNNER END =====");
} finally { } finally {
......
...@@ -286,7 +286,7 @@ public class MacroPlannerOptimizer { ...@@ -286,7 +286,7 @@ public class MacroPlannerOptimizer {
// ========== 1. 前置参数校验 ========== // ========== 1. 前置参数校验 ==========
List<KpiSetting> w = data.getKpiSettings(); List<KpiSetting> w = data.getKpiSettings();
List<StrategyLevel> levels=new ArrayList<>(); List<StrategyLevel> levels=new ArrayList<>();
if (w == null) { if (w == null||w.size()==0) {
levels = defineLevels(data.getKpiWeights()); levels = defineLevels(data.getKpiWeights());
}else { }else {
levels = getKpiLevels(w); levels = getKpiLevels(w);
...@@ -342,16 +342,18 @@ public class MacroPlannerOptimizer { ...@@ -342,16 +342,18 @@ public class MacroPlannerOptimizer {
double bestBound = model.getSolver().objective().bestBound(); double bestBound = model.getSolver().objective().bestBound();
writeLog(" SCIP Status : %s%n", status); writeLog(" SCIP Status : %s%n", status);
writeLog(" Solving Time (sec) : %.2f%n", levelElapsedMs / 1000.0); 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); writeLog(" Best Bound : %+.6e%n", bestBound);
double gap = Math.abs(optimalValue - bestBound) / Math.abs(optimalValue) * 100; double gap = Math.abs(optimalValue - bestBound) / Math.abs(optimalValue) * 100;
writeLog("gap:%.2f", gap); writeLog("gap:%.2f", gap);
// 输出当前层各KPI值 // 输出当前层各KPI值
for (StrategyLevel.KPIEntry kpi : level.getKpis()) { for (StrategyLevel.KPIEntry kpi : level.getKpis()) {
double kpiValue = kpi.variable.solutionValue(); double kpiValue = kpi.variable.solutionValue();
double penalty = kpi.effectiveCoefficient() * kpiValue; double contribution = kpi.effectiveCoefficient() * kpiValue;
writeLog(" %s: %.2f (系数=%.1f, 惩罚=%.2f)%n",
kpi.name, kpiValue, kpi.effectiveCoefficient(), penalty); System.out.printf(" %s: %.2f (系数=%+.1f, 得分贡献=%+.2f)%n",
kpi.name, kpiValue, kpi.effectiveCoefficient(), contribution);
} }
levelResults.add(new LevelResult(level, optimalValue, status)); levelResults.add(new LevelResult(level, optimalValue, status));
...@@ -431,14 +433,19 @@ public class MacroPlannerOptimizer { ...@@ -431,14 +433,19 @@ public class MacroPlannerOptimizer {
private List<StrategyLevel> defineLevels(KPIWeights w) { private List<StrategyLevel> defineLevels(KPIWeights w) {
List<StrategyLevel> levels = new ArrayList<>(); 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%) === // === Level 1: 需求满足 (最高优先级, 严格分层, slack=0%) ===
StrategyLevel l1 = new StrategyLevel(1, "需求满足", 0.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); 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(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); levels.add(l2);
// === Level 3: 业务KPI (允许 5% 退化, slack=5%) === // === Level 3: 业务KPI (允许 5% 退化, slack=5%) ===
...@@ -472,7 +479,10 @@ public class MacroPlannerOptimizer { ...@@ -472,7 +479,10 @@ public class MacroPlannerOptimizer {
TreeMap::new, TreeMap::new,
Collectors.toList() 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()) { for (Map.Entry<Integer, List<KpiSetting>> entry : groupByLevelMap.entrySet()) {
Integer currentLevel = entry.getKey(); Integer currentLevel = entry.getKey();
List<KpiSetting> kpiGroup = entry.getValue(); List<KpiSetting> kpiGroup = entry.getValue();
...@@ -480,7 +490,7 @@ public class MacroPlannerOptimizer { ...@@ -480,7 +490,7 @@ public class MacroPlannerOptimizer {
// 遍历当前分组里面每一个KPI // 遍历当前分组里面每一个KPI
for (KpiSetting kpi : kpiGroup) { 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 { ...@@ -38,8 +38,12 @@ public class KpiAggregator {
public static void build(MacroPlannerModel model, TestDataBuilder data) { public static void build(MacroPlannerModel model, TestDataBuilder data) {
double inf = MPSolver.infinity(); double inf = MPSolver.infinity();
// TotalFulfillment = Σ DemandSlack // TotalFulfillment = Σ SalesDemandQty (Quintiq fulfillment bonus KPI)
model.setTotalFulfillment(createSumKpi(model, "TotalFulfillment", model.setTotalFulfillment(createSumKpi(model, "TotalFulfillment",
model.getSalesDemandQtyVars()));
// TotalSlack = Σ DemandSlack (Quintiq Slack KPI, 高优先级惩罚)
model.setTotalSlack(createSumKpi(model, "TotalSlack",
model.getDemandSlackVars())); model.getDemandSlackVars()));
// TotalLotSize = Σ PTLotSizeOver + Σ PTLotSizeUnder // TotalLotSize = Σ PTLotSizeOver + Σ PTLotSizeUnder
...@@ -66,10 +70,16 @@ public class KpiAggregator { ...@@ -66,10 +70,16 @@ public class KpiAggregator {
model.setTotalUnitCapacity(createSumKpi(model, "TotalUnitCapacity", model.setTotalUnitCapacity(createSumKpi(model, "TotalUnitCapacity",
model.getCapacityOverloadedVars())); model.getCapacityOverloadedVars()));
// TotalMinimumUnitCapacity = Σ CapacityNotMet (Quintiq 最小产能未满足 KPI)
model.setTotalMinimumUnitCapacity(createSumKpi(model, "TotalMinUnitCapacity",
model.getCapacityNotMetVars()));
// TotalSupplyTarget = Σ SupplyTargetQtyUnder // TotalSupplyTarget = Σ SupplyTargetQtyUnder
model.setTotalSupplyTarget(createSumKpi(model, "TotalSupplyTarget", model.setTotalSupplyTarget(createSumKpi(model, "TotalSupplyTarget",
model.getSupplyTargetQtyUnderVars())); model.getSupplyTargetQtyUnderVars()));
// TotalMinSupply = Σ MinSupplyQtyUnder // TotalMinSupply = Σ MinSupplyQtyUnder
model.setTotalMinSupply(createSumKpi(model, "TotalMinSupply", model.setTotalMinSupply(createSumKpi(model, "TotalMinSupply",
model.getMinSupplyQtyUnderVars())); model.getMinSupplyQtyUnderVars()));
......
...@@ -80,12 +80,19 @@ public class MacroPlannerModel { ...@@ -80,12 +80,19 @@ public class MacroPlannerModel {
private final Map<String, MPVariable> ptLotSizeUnderVars = new HashMap<>(); private final Map<String, MPVariable> ptLotSizeUnderVars = new HashMap<>();
// ==================== KPI 汇总变量 ==================== // ==================== KPI 汇总变量 ====================
private MPVariable totalSlack;
private MPVariable totalFulfillment; private MPVariable totalFulfillment;
private MPVariable totalLotSize; private MPVariable totalLotSize;
private MPVariable totalMaxInventoryLevel; private MPVariable totalMaxInventoryLevel;
private MPVariable totalMinInventoryLevel; private MPVariable totalMinInventoryLevel;
private MPVariable totalTargetInvLevel; private MPVariable totalTargetInvLevel;
private MPVariable totalUnitCapacity; private MPVariable totalUnitCapacity;
private MPVariable totalMinimumUnitCapacity;
private MPVariable totalSupplyTarget; private MPVariable totalSupplyTarget;
private MPVariable totalMinSupply; private MPVariable totalMinSupply;
private MPVariable totalMaxSupply; private MPVariable totalMaxSupply;
...@@ -123,6 +130,7 @@ public class MacroPlannerModel { ...@@ -123,6 +130,7 @@ public class MacroPlannerModel {
} }
// ==================== KPI 变量 setter (由 KpiAggregator 调用) ==================== // ==================== KPI 变量 setter (由 KpiAggregator 调用) ====================
public void setTotalSlack(MPVariable v) { this.totalSlack = v; }
public void setTotalFulfillment(MPVariable v) { this.totalFulfillment = v; } public void setTotalFulfillment(MPVariable v) { this.totalFulfillment = v; }
public void setTotalLotSize(MPVariable v) { this.totalLotSize = v; } public void setTotalLotSize(MPVariable v) { this.totalLotSize = v; }
...@@ -130,6 +138,9 @@ public class MacroPlannerModel { ...@@ -130,6 +138,9 @@ public class MacroPlannerModel {
public void setTotalMinInventoryLevel(MPVariable v) { this.totalMinInventoryLevel = v; } public void setTotalMinInventoryLevel(MPVariable v) { this.totalMinInventoryLevel = v; }
public void setTotalTargetInvLevel(MPVariable v) { this.totalTargetInvLevel = v; } public void setTotalTargetInvLevel(MPVariable v) { this.totalTargetInvLevel = v; }
public void setTotalUnitCapacity(MPVariable v) { this.totalUnitCapacity = 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 setTotalSupplyTarget(MPVariable v) { this.totalSupplyTarget = v; }
public void setTotalMinSupply(MPVariable v) { this.totalMinSupply = v; } public void setTotalMinSupply(MPVariable v) { this.totalMinSupply = v; }
public void setTotalMaxSupply(MPVariable v) { this.totalMaxSupply = v; } public void setTotalMaxSupply(MPVariable v) { this.totalMaxSupply = v; }
...@@ -150,6 +161,8 @@ public class MacroPlannerModel { ...@@ -150,6 +161,8 @@ public class MacroPlannerModel {
public Map<String, MPVariable> getDemandFulfillmentVars() { return demandFulfillmentVars; } public Map<String, MPVariable> getDemandFulfillmentVars() { return demandFulfillmentVars; }
public Map<String, MPVariable> getCapacityOverloadedVars() { return capacityOverloadedVars; } public Map<String, MPVariable> getCapacityOverloadedVars() { return capacityOverloadedVars; }
public Map<String, MPVariable> getCapacityNotMetVars() { return capacityNotMetVars; } public Map<String, MPVariable> getCapacityNotMetVars() { return capacityNotMetVars; }
public Map<String, MPVariable> getMinInvQtyUnderVars() { return minInvQtyUnderVars; } public Map<String, MPVariable> getMinInvQtyUnderVars() { return minInvQtyUnderVars; }
public Map<String, MPVariable> getMaxInvQtyOverVars() { return maxInvQtyOverVars; } public Map<String, MPVariable> getMaxInvQtyOverVars() { return maxInvQtyOverVars; }
public Map<String, MPVariable> getInvQtyUnderTargetVars() { return invQtyUnderTargetVars; } public Map<String, MPVariable> getInvQtyUnderTargetVars() { return invQtyUnderTargetVars; }
...@@ -159,12 +172,16 @@ public class MacroPlannerModel { ...@@ -159,12 +172,16 @@ public class MacroPlannerModel {
public Map<String, MPVariable> getPtLotSizeOverVars() { return ptLotSizeOverVars; } public Map<String, MPVariable> getPtLotSizeOverVars() { return ptLotSizeOverVars; }
public Map<String, MPVariable> getPtLotSizeUnderVars() { return ptLotSizeUnderVars; } public Map<String, MPVariable> getPtLotSizeUnderVars() { return ptLotSizeUnderVars; }
public MPVariable getTotalSlack() { return totalSlack; }
public MPVariable getTotalFulfillment() { return totalFulfillment; } public MPVariable getTotalFulfillment() { return totalFulfillment; }
public MPVariable getTotalLotSize() { return totalLotSize; } public MPVariable getTotalLotSize() { return totalLotSize; }
public MPVariable getTotalMaxInventoryLevel() { return totalMaxInventoryLevel; } public MPVariable getTotalMaxInventoryLevel() { return totalMaxInventoryLevel; }
public MPVariable getTotalMinInventoryLevel() { return totalMinInventoryLevel; } public MPVariable getTotalMinInventoryLevel() { return totalMinInventoryLevel; }
public MPVariable getTotalTargetInvLevel() { return totalTargetInvLevel; } public MPVariable getTotalTargetInvLevel() { return totalTargetInvLevel; }
public MPVariable getTotalUnitCapacity() { return totalUnitCapacity; } public MPVariable getTotalUnitCapacity() { return totalUnitCapacity; }
public MPVariable getTotalMinimumUnitCapacity() { return totalMinimumUnitCapacity; }
public MPVariable getTotalSupplyTarget() { return totalSupplyTarget; } public MPVariable getTotalSupplyTarget() { return totalSupplyTarget; }
public MPVariable getTotalMinSupply() { return totalMinSupply; } public MPVariable getTotalMinSupply() { return totalMinSupply; }
public MPVariable getTotalMaxSupply() { return totalMaxSupply; } public MPVariable getTotalMaxSupply() { return totalMaxSupply; }
...@@ -183,6 +200,9 @@ public class MacroPlannerModel { ...@@ -183,6 +200,9 @@ public class MacroPlannerModel {
case UnitCapacity: case UnitCapacity:
kpivar = totalUnitCapacity; kpivar = totalUnitCapacity;
break; break;
case MinUnitCapacity:
kpivar = totalMinimumUnitCapacity;
break;
case LotSize: case LotSize:
kpivar = totalLotSize; kpivar = totalLotSize;
break; break;
......
...@@ -9,23 +9,28 @@ import java.util.stream.Collectors; ...@@ -9,23 +9,28 @@ import java.util.stream.Collectors;
* 时间:2026-09-22 * 时间:2026-09-22
*/ */
public enum KpiLib { public enum KpiLib {
Fulfillment("需求满足"), Fulfillment("需求满足",true),
UnitCapacity("产能"), UnitCapacity("产能",false),
LotSize("批次"), MinUnitCapacity("最小化产能",false),
TargetInvLevel("目标库存"), LotSize("批次",false),
SupplyTarget("供应目标"), TargetInvLevel("目标库存",false),
SalesDemandPriority("销售优先级"), SupplyTarget("供应目标",false),
MaxInventoryLevel("最大库存"), SalesDemandPriority("销售优先级",false),
MinInventoryLevel("最小库存"), MaxInventoryLevel("最大库存",false),
MinSupply("最小供应"), MinInventoryLevel("最小库存",false),
MaxSupply("最大供应"); MinSupply("最小供应",false),
MaxSupply("最大供应",false);
private final String en; private final String en;
private final String cn; private final String cn;
KpiLib(String cn) { /** 是否为收益 KPI(越大越好)。 */
public final boolean isBenefit;
KpiLib(String cn,boolean isBenefit) {
this.en = this.name(); this.en = this.name();
this.cn = cn; this.cn = cn;
this.isBenefit=isBenefit;
} }
public String getEn() { public String getEn() {
...@@ -36,6 +41,10 @@ public enum KpiLib { ...@@ -36,6 +41,10 @@ public enum KpiLib {
return cn; return cn;
} }
public boolean getIsBenefit() {
return isBenefit;
}
/** /**
* 转为 Map<英文编码,中文名称>,用于批量翻译、表头转换 * 转为 Map<英文编码,中文名称>,用于批量翻译、表头转换
*/ */
......
...@@ -168,18 +168,18 @@ public class ObjectiveBuilder { ...@@ -168,18 +168,18 @@ public class ObjectiveBuilder {
for (StrategyLevel.KPIEntry kpi : level.getKpis()) { for (StrategyLevel.KPIEntry kpi : level.getKpis()) {
objective.setCoefficient(kpi.variable, kpi.effectiveCoefficient()); objective.setCoefficient(kpi.variable, kpi.effectiveCoefficient());
} }
objective.setMinimization(); objective.setMaximization();
} }
/** /**
* 添加层级边界约束 — 限制上层目标值不超过最优值 × (1 + slack)。 * 添加层级边界约束 — 限制上层得分不低于最优值减去允许松弛。
* *
* <p>该约束确保在求解下层 KPI 时, 上层 KPI 不会退化超过允许范围。 * <p>该约束确保在求解下层 KPI 时, 上层 KPI 不会退化超过允许范围。
* 对应 Quintiq 中 StrategyLevel 的 HierarchicalSolver 约束。</p> * 对应 Quintiq 中 StrategyLevel 的 HierarchicalSolver 约束。</p>
* *
* <h3>数学公式</h3> * <h3>数学公式</h3>
* <pre> * <pre>
* Σ (effectiveCoeff × KPI_variable) ≤ optimalValue + |optimalValue| × relativeGoalSlack * Σ (effectiveCoeff × KPI_variable) ≥ optimalValue - |optimalValue × relativeGoalSlack|
* </pre> * </pre>
* *
* @param model 模型容器 * @param model 模型容器
...@@ -192,10 +192,10 @@ public class ObjectiveBuilder { ...@@ -192,10 +192,10 @@ public class ObjectiveBuilder {
if (level.getRelativeGoalSlack() < 0.0) return; // 负松弛表示不约束 if (level.getRelativeGoalSlack() < 0.0) return; // 负松弛表示不约束
MPSolver solver = model.getSolver(); MPSolver solver = model.getSolver();
double upperBound = computeUpperBound(optimalValue, level.getRelativeGoalSlack()); double lowerBound = optimalValue - Math.abs(optimalValue * level.getRelativeGoalSlack());
MPConstraint bound = solver.makeConstraint( MPConstraint bound = solver.makeConstraint(
-MPSolver.infinity(), upperBound, lowerBound, MPSolver.infinity(),
"HierLevel" + level.getLevel() + "_Bound"); "HierLevel" + level.getLevel() + "_Bound");
for (StrategyLevel.KPIEntry kpi : level.getKpis()) { for (StrategyLevel.KPIEntry kpi : level.getKpis()) {
......
...@@ -60,8 +60,8 @@ public class StrategyLevel { ...@@ -60,8 +60,8 @@ public class StrategyLevel {
/** 该 KPI 在当前层级内的权重 */ /** 该 KPI 在当前层级内的权重 */
public final double weight; public final double weight;
/** 是否为负向 KPI (越小越好 = 正常惩罚项; false = 正常惩罚项) */ /** 是否为收益 KPI(越大越好)。 */
public final boolean isNegative; public final boolean isBenefit;
/** 编号 */ /** 编号 */
public final String code; public final String code;
...@@ -69,20 +69,20 @@ public class StrategyLevel { ...@@ -69,20 +69,20 @@ public class StrategyLevel {
/** KPI 名称 (用于日志) */ /** KPI 名称 (用于日志) */
public final String name; 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.code = code;
this.name = name; this.name = name;
this.variable = variable; this.variable = variable;
this.weight = weight; this.weight = weight;
this.isNegative = isNegative; this.isBenefit = isBenefit;
} }
/** /**
* 计算该 KPI 在目标函数中的实际系数。 * 计算该 KPI 在目标函数中的实际系数。
* 负向 KPI (如 SalesDemandPriority) 使用负系数实现最大化。 * 收益 KPI 使用正系数;惩罚 KPI 使用负系数。
*/ */
public double effectiveCoefficient() { public double effectiveCoefficient() {
return isNegative ? -weight : weight; return isBenefit ? weight : -weight;
} }
} }
...@@ -116,11 +116,11 @@ public class StrategyLevel { ...@@ -116,11 +116,11 @@ public class StrategyLevel {
* @param name KPI 名称 * @param name KPI 名称
* @param variable KPI 汇总变量 * @param variable KPI 汇总变量
* @param weight 权重 (0 = 跳过) * @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) { if (weight > 0.0 && variable != null) {
kpis.add(new KPIEntry(code,name, variable, weight, isNegative)); kpis.add(new KPIEntry(code,name, variable, weight, isBenefit));
} }
} }
......
...@@ -143,6 +143,12 @@ public class ResultWriter { ...@@ -143,6 +143,12 @@ public class ResultWriter {
String fileName =resultDir.getAbsolutePath()+ "\\salesdemand.parquet"; String fileName =resultDir.getAbsolutePath()+ "\\salesdemand.parquet";
return fileName; return fileName;
} }
public String getOptimizationSalesDemandReasons(String sceneId) {
File resultDir = getResultDirectory(sceneId);
String fileName =resultDir.getAbsolutePath()+ "\\salesdemandReasons.parquet";
return fileName;
}
public String getOptimizationSalesDemandSummarie(String sceneId) { public String getOptimizationSalesDemandSummarie(String sceneId) {
File resultDir = getResultDirectory(sceneId); File resultDir = getResultDirectory(sceneId);
String fileName =resultDir.getAbsolutePath()+ "\\salesdemandsummarie.parquet"; String fileName =resultDir.getAbsolutePath()+ "\\salesdemandsummarie.parquet";
...@@ -213,6 +219,11 @@ public class ResultWriter { ...@@ -213,6 +219,11 @@ public class ResultWriter {
writeLog("writesalesDemand"); writeLog("writesalesDemand");
String salesDemandPath = getOptimizationSalesDemand(sceneId); String salesDemandPath = getOptimizationSalesDemand(sceneId);
parquetUtil.write(result.getSalesDemands(), salesDemandPath, SalesDemandResult.class); parquetUtil.write(result.getSalesDemands(), salesDemandPath, SalesDemandResult.class);
String salesDemandReasonPath = getOptimizationSalesDemandReasons(sceneId);
parquetUtil.write(result.getSaleDemandReasons(), salesDemandReasonPath, RiskItem.class);
// List<PispipResult> pispips = parquetUtil.readAll(pispiPath, PispipResult.class); // List<PispipResult> pispips = parquetUtil.readAll(pispiPath, PispipResult.class);
String salesDemandSummariePath = getOptimizationSalesDemandSummarie(sceneId); String salesDemandSummariePath = getOptimizationSalesDemandSummarie(sceneId);
parquetUtil.write(result.getSaleSummarieDemands(), salesDemandSummariePath, SalesDemandResult.class); parquetUtil.write(result.getSaleSummarieDemands(), salesDemandSummariePath, SalesDemandResult.class);
...@@ -529,7 +540,9 @@ public class ResultWriter { ...@@ -529,7 +540,9 @@ public class ResultWriter {
writeLog("PeriodTasks"); writeLog("PeriodTasks");
buildPeriodTasks(sceneId,result); buildPeriodTasks(sceneId,result);
writeLog("SalesDemands"); writeLog("SalesDemands");
buildSalesDemands(result); List<RiskItem> unmetReasons=new ArrayList<>();
buildSalesDemands(result,unmetReasons);
result.setSaleDemandReasons(unmetReasons);
writeLog("Pispips"); writeLog("Pispips");
buildPispips(result); buildPispips(result);
writeLog("UnitCapacitie"); writeLog("UnitCapacitie");
...@@ -713,7 +726,7 @@ public class ResultWriter { ...@@ -713,7 +726,7 @@ public class ResultWriter {
// ==================== SalesDemandResult ==================== // ==================== SalesDemandResult ====================
private void buildSalesDemands(OptimizationResult result) { private void buildSalesDemands(OptimizationResult result,List<RiskItem> unmetReasons) {
for (SalesDemand sd : data.getSalesDemands()) { for (SalesDemand sd : data.getSalesDemands()) {
SalesDemandResult sr = new SalesDemandResult(); SalesDemandResult sr = new SalesDemandResult();
sr.setSalesDemandId(sd.getDemandOrderId()==null? sd.getKey():sd.getDemandOrderId()); sr.setSalesDemandId(sd.getDemandOrderId()==null? sd.getKey():sd.getDemandOrderId());
...@@ -740,10 +753,10 @@ public class ResultWriter { ...@@ -740,10 +753,10 @@ public class ResultWriter {
sr.setDemandSlack(solutionValue(model.getDemandSlackVars(), invKey).setScale(3, RoundingMode.HALF_UP).doubleValue()); sr.setDemandSlack(solutionValue(model.getDemandSlackVars(), invKey).setScale(3, RoundingMode.HALF_UP).doubleValue());
// 未完成原因分析 // 未完成原因分析
if (unmet > 0.001) { if (unmet > 0.001) {
// analyzeUnmetReasons(sd, sr.getUnmetReasons()); analyzeUnmetReasons(sd, unmetReasons);
} }
// 风险分析 // 风险分析
// analyzeRisks(sd, sr.getRisks()); analyzeRisks(sd, unmetReasons);
result.getSalesDemands().add(sr); result.getSalesDemands().add(sr);
} }
...@@ -782,7 +795,7 @@ public class ResultWriter { ...@@ -782,7 +795,7 @@ public class ResultWriter {
* <li>无生产工序: 纯采购品, 依赖库存/在途</li> * <li>无生产工序: 纯采购品, 依赖库存/在途</li>
* </ol> * </ol>
*/ */
private void analyzeUnmetReasons(SalesDemand sd, java.util.List<String> reasons) { private void analyzeUnmetReasons(SalesDemand sd, List<RiskItem> reasons) {
Product prod = sd.getProduct(); Product prod = sd.getProduct();
StockingPoint sp = sd.getStockingPoint(); StockingPoint sp = sd.getStockingPoint();
Period p = sd.getPeriod(); Period p = sd.getPeriod();
...@@ -804,22 +817,25 @@ public class ResultWriter { ...@@ -804,22 +817,25 @@ public class ResultWriter {
} }
double available = openingInv + totalArrived.setScale(2, RoundingMode.HALF_UP).doubleValue(); double available = openingInv + totalArrived.setScale(2, RoundingMode.HALF_UP).doubleValue();
if (available < demandQty) { if (available < demandQty) {
addReasonWithDetail(reasons, "库存耗尽", addReasonWithDetail(reasons, sd,"库存耗尽",
"(可用" + fmt(available) + "件, 需求" + fmt(demandQty) + "件)"); "(可用" + fmt(available) + "件, 需求" + fmt(demandQty) + "件)");
} }
// 2. 产能瓶颈: 所有生产该产品的单元满负荷 (利用率 > 95%) // 2. 产能瓶颈: 所有生产该产品的单元满负荷 (利用率 > 95%)
boolean hasAnyUnit = false; boolean hasAnyUnit = false;
boolean allUnitsAtMax = true; boolean allUnitsAtMax = true;
String unitnames="";
for (Operation op : data.getOperationsProducing(prod.getId(), sp.getId())) { for (Operation op : data.getOperationsProducing(prod.getId(), sp.getId())) {
for (UnitOperation uo : op.getUnitOperations()) { for (UnitOperation uo : op.getUnitOperations()) {
hasAnyUnit = true; hasAnyUnit = true;
UnitPeriod up = data.getUnitPeriod(uo.getUnitId(), p); UnitPeriod up = data.getUnitPeriod(uo.getUnitId(), p);
if (up == null || up.isUnlimited()) continue; if (up == null || up.isUnlimited()) continue;
BigDecimal totalUsed = BigDecimal.ZERO; BigDecimal totalUsed = BigDecimal.ZERO;
for (Operation unitOp : data.getOperationsByUnitId(uo.getUnitId())) { for (Operation unitOp : data.getOperationsByUnitId(uo.getUnitId())) {
for (UnitOperation unitUo : unitOp.getUnitOperations()) { for (UnitOperation unitUo : unitOp.getUnitOperations()) {
if (!unitUo.getUnitId().equals(uo.getUnitId())) continue; if (!unitUo.getUnitId().equals(uo.getUnitId())) continue;
unitnames+= unitUo.getUnitName()+";";
totalUsed =totalUsed.add( solutionValue(model.getPtQtyVars(), totalUsed =totalUsed.add( solutionValue(model.getPtQtyVars(),
unitOp.ptQtyKey(unitUo, p.getIndex())).multiply(BigDecimal.valueOf(unitUo.getCapacityCoeff()) ) ); unitOp.ptQtyKey(unitUo, p.getIndex())).multiply(BigDecimal.valueOf(unitUo.getCapacityCoeff()) ) );
} }
...@@ -830,7 +846,8 @@ public class ResultWriter { ...@@ -830,7 +846,8 @@ public class ResultWriter {
if (!allUnitsAtMax) break; if (!allUnitsAtMax) break;
} }
if (hasAnyUnit && allUnitsAtMax) { if (hasAnyUnit && allUnitsAtMax) {
reasons.add("产能瓶颈(所有产线满负荷)"); addReasonWithDetail(reasons,sd,"产能瓶颈(所有产线满负荷)",unitnames);
} }
// 3. 原材料短缺: BOM 输入物料上期库存见底 // 3. 原材料短缺: BOM 输入物料上期库存见底
...@@ -845,8 +862,12 @@ public class ResultWriter { ...@@ -845,8 +862,12 @@ public class ResultWriter {
InventorySpec inputSpec = data.getInventorySpecFor(inputProd, inputSp, p); InventorySpec inputSpec = data.getInventorySpecFor(inputProd, inputSp, p);
double minInv = inputSpec != null ? inputSpec.getMinLevel() : 0; double minInv = inputSpec != null ? inputSpec.getMinLevel() : 0;
if (prevInputInv <= minInv + 0.001) { if (prevInputInv <= minInv + 0.001) {
String r = "原材料短缺(" + inputProd.getId() + "库存见底)"; String r = "原材料短缺(" + inputProd.getCode() + "库存见底)";
if (!reasons.contains(r)) reasons.add(r); if (!reasons.contains(r))
{
addReasonWithDetail(reasons,sd,"原材料短缺",r);
}
} }
} }
} }
...@@ -861,8 +882,10 @@ public class ResultWriter { ...@@ -861,8 +882,10 @@ public class ResultWriter {
totalProduced=totalProduced.add( solutionValue(model.getPtQtyVars(), op.ptQtyKey(uo, p.getIndex()))); totalProduced=totalProduced.add( solutionValue(model.getPtQtyVars(), op.ptQtyKey(uo, p.getIndex())));
} }
if (totalProduced.doubleValue() > 0.001) { if (totalProduced.doubleValue() > 0.001) {
reasons.add("提前期限制(生产" + fmt(totalProduced.doubleValue()) + "件, 需" String r="提前期限制(生产" + fmt(totalProduced.doubleValue()) + "件, 需"
+ op.getLeadTimeDays() + "天后到货)"); + op.getLeadTimeDays() + "天后到货)";
addReasonWithDetail(reasons,sd,"提前期限制",r);
} }
} }
} }
...@@ -870,17 +893,23 @@ public class ResultWriter { ...@@ -870,17 +893,23 @@ public class ResultWriter {
// 5. 无生产工序 // 5. 无生产工序
boolean hasOperation = !data.getOperationsProducingProduct(prod.getId()).isEmpty(); boolean hasOperation = !data.getOperationsProducingProduct(prod.getId()).isEmpty();
if (!hasOperation) { if (!hasOperation) {
reasons.add("无生产工序(纯采购品, 依赖库存/在途)"); addReasonWithDetail(reasons,sd,"无生产工序","无生产工序(纯采购品, 依赖库存/在途)");
} }
if (reasons.isEmpty()) { if (reasons.isEmpty()) {
reasons.add("供需缺口(可用库存不足)"); addReasonWithDetail(reasons,sd,"供需缺口","供需缺口(可用库存不足)");
} }
} }
private void addReasonWithDetail(java.util.List<String> reasons, String title, String detail) {
reasons.add(title);
reasons.add(detail); private void addReasonWithDetail(java.util.List<RiskItem> reasons,SalesDemand sd, String title, String detail) {
addReasonWithDetail(reasons,sd,1,"",title,detail);
}
private void addReasonWithDetail(java.util.List<RiskItem> reasons,SalesDemand sd,int type,String level, String title, String detail) {
RiskItem reason=new RiskItem(sd.getDemandOrderId(),sd.getPeriod().getIndex(),type,level,title,detail);
reasons.add(reason);
} }
/** /**
...@@ -905,7 +934,7 @@ public class ResultWriter { ...@@ -905,7 +934,7 @@ public class ResultWriter {
double endingInv = solutionValue(model.getInvQtyVars(), invKey).setScale(3, RoundingMode.HALF_UP).doubleValue(); double endingInv = solutionValue(model.getInvQtyVars(), invKey).setScale(3, RoundingMode.HALF_UP).doubleValue();
InventorySpec spec = data.getInventorySpecFor(prod, sp, p); InventorySpec spec = data.getInventorySpecFor(prod, sp, p);
if (spec != null && spec.hasMinLevel() && endingInv < spec.getMinLevel() - 0.001) { if (spec != null && spec.hasMinLevel() && endingInv < spec.getMinLevel() - 0.001) {
addRisk(risks, "高", "安全库存被消耗(期末" + fmt(endingInv) + "件 < 最小" + fmt(spec.getMinLevel()) + "件)"); addRisk(risks,sd, "高", "安全库存被消耗(期末" + fmt(endingInv) + "件 < 最小" + fmt(spec.getMinLevel()) + "件)");
} }
// 2. 产线高负荷 (> 90%) // 2. 产线高负荷 (> 90%)
...@@ -926,7 +955,7 @@ public class ResultWriter { ...@@ -926,7 +955,7 @@ public class ResultWriter {
.doubleValue() .doubleValue()
: 0D; if (util > 0.90) { : 0D; if (util > 0.90) {
String level = util > 0.95 ? "高" : "低"; String level = util > 0.95 ? "高" : "低";
addRisk(risks, level, "产线高负荷(" + uo.getUnitId() + "利用率" addRisk(risks,sd, level, "产线高负荷(" + uo.getUnitId() + "利用率"
+ String.format("%.0f%%", util * 100) + ")"); + String.format("%.0f%%", util * 100) + ")");
} }
} }
...@@ -938,7 +967,7 @@ public class ResultWriter { ...@@ -938,7 +967,7 @@ public class ResultWriter {
unitCount += op.getUnitOperations().size(); unitCount += op.getUnitOperations().size();
} }
if (unitCount == 1) { if (unitCount == 1) {
addRisk(risks, "低", "单一供应源(仅1条产线可生产)"); addRisk(risks,sd, "低", "单一供应源(仅1条产线可生产)");
} }
// 4. 原材料提前期长: 关键 BOM 物料 leadTime 长 // 4. 原材料提前期长: 关键 BOM 物料 leadTime 长
...@@ -948,7 +977,7 @@ public class ResultWriter { ...@@ -948,7 +977,7 @@ public class ResultWriter {
Product inputProd = input.getInputProduct(); Product inputProd = input.getInputProduct();
for (Operation upOp : data.getOperationsProducingProduct(inputProd.getId())) { for (Operation upOp : data.getOperationsProducingProduct(inputProd.getId())) {
if (upOp.getLeadTimeDays() >= 2) { if (upOp.getLeadTimeDays() >= 2) {
addRisk(risks, "低", "原材料提前期长(" + inputProd.getId() + "采购需" addRisk(risks, sd,"低", "原材料提前期长(" + inputProd.getId() + "采购需"
+ upOp.getLeadTimeDays() + "天)"); + upOp.getLeadTimeDays() + "天)");
} }
} }
...@@ -959,16 +988,16 @@ public class ResultWriter { ...@@ -959,16 +988,16 @@ public class ResultWriter {
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 rate = sd.getQuantity() > 0 ? fulfilled / sd.getQuantity() : 1.0; double rate = sd.getQuantity() > 0 ? fulfilled / sd.getQuantity() : 1.0;
if (sd.getPriority() > 1.0 && rate < 0.999) { if (sd.getPriority() > 1.0 && rate < 0.999) {
addRisk(risks, "高", "高优先级需求满足率" + String.format("%.0f%%", rate * 100)); addRisk(risks,sd, "高", "高优先级需求满足率" + String.format("%.0f%%", rate * 100));
} }
} }
/** 按描述去重后添加风险项 */ /** 按描述去重后添加风险项 */
private void addRisk(java.util.List<RiskItem> risks, String level, String description) { private void addRisk(java.util.List<RiskItem> risks,SalesDemand sd, String level, String description) {
for (RiskItem r : risks) { for (RiskItem r : risks) {
if (r.getDescription().equals(description)) return; if (r.getDescription().equals(description)) return;
} }
risks.add(new RiskItem(level, description)); addReasonWithDetail(risks,sd,2,level,"",description);
} }
/** 格式化数值: 去掉多余小数位 */ /** 格式化数值: 去掉多余小数位 */
private String fmt(double v) { private String fmt(double v) {
...@@ -1606,7 +1635,7 @@ public class ResultWriter { ...@@ -1606,7 +1635,7 @@ public class ResultWriter {
jb.key("fulfillmentRate").val(sr.getFulfillmentRate()); jb.key("fulfillmentRate").val(sr.getFulfillmentRate());
jb.key("priority").val(sr.getPriority()); jb.key("priority").val(sr.getPriority());
jb.key("unmetReasons").arr(); jb.key("unmetReasons").arr();
for (String r : sr.getUnmetReasons()) jb.val(r); // for (String r : sr.getUnmetReasons()) jb.val(r);
jb.endArr(); jb.endArr();
jb.key("risks").arr(); jb.key("risks").arr();
for (RiskItem r : sr.getRisks()) { for (RiskItem r : sr.getRisks()) {
...@@ -1698,6 +1727,7 @@ public class ResultWriter { ...@@ -1698,6 +1727,7 @@ public class ResultWriter {
.key("rawValue").val(e.rawValue) .key("rawValue").val(e.rawValue)
.key("weight").valOpt(e.weight) .key("weight").valOpt(e.weight)
.key("penalty").valOpt(e.penalty) .key("penalty").valOpt(e.penalty)
.key("contribution").valOpt(e.contribution)
.key("isBenefit").val(e.isBenefit) .key("isBenefit").val(e.isBenefit)
.endObj(); .endObj();
} }
......
...@@ -693,37 +693,37 @@ public class SolutionPrinter { ...@@ -693,37 +693,37 @@ public class SolutionPrinter {
// 计算并输出加权总惩罚 (等价于单目标函数值) // 计算并输出加权总惩罚 (等价于单目标函数值)
writeLog(""); writeLog("");
writeLog("--- 最终 KPI 值 (加权总惩罚) ---"); // writeLog("--- 最终 KPI 值 (加权总惩罚) ---");
double fulfillment = model.getTotalFulfillment().solutionValue(); // double fulfillment = model.getTotalFulfillment().solutionValue();
double lotSize = model.getTotalLotSize().solutionValue(); // double lotSize = model.getTotalLotSize().solutionValue();
double maxInv = model.getTotalMaxInventoryLevel().solutionValue(); // double maxInv = model.getTotalMaxInventoryLevel().solutionValue();
double minInv = model.getTotalMinInventoryLevel().solutionValue(); // double minInv = model.getTotalMinInventoryLevel().solutionValue();
double targetInv = model.getTotalTargetInvLevel().solutionValue(); // double targetInv = model.getTotalTargetInvLevel().solutionValue();
double capacity = model.getTotalUnitCapacity().solutionValue(); // double capacity = model.getTotalUnitCapacity().solutionValue();
double supplyTarget = model.getTotalSupplyTarget().solutionValue(); // double supplyTarget = model.getTotalSupplyTarget().solutionValue();
double minSupply = model.getTotalMinSupply().solutionValue(); // double minSupply = model.getTotalMinSupply().solutionValue();
double maxSupply = model.getTotalMaxSupply().solutionValue(); // double maxSupply = model.getTotalMaxSupply().solutionValue();
double salesPriority = model.getTotalSalesDemandPriority().solutionValue(); // double salesPriority = model.getTotalSalesDemandPriority().solutionValue();
double totalPenalty = fulfillment * weights.getOrDefault(KpiLib.Fulfillment.getEn(),0d) // double totalPenalty = fulfillment * weights.getOrDefault(KpiLib.Fulfillment.getEn(),0d)
+ lotSize * weights.getOrDefault(KpiLib.LotSize.getEn(),0d) // + lotSize * weights.getOrDefault(KpiLib.LotSize.getEn(),0d)
+ maxInv * weights.getOrDefault(KpiLib.MinInventoryLevel.getEn(),0d) // + maxInv * weights.getOrDefault(KpiLib.MinInventoryLevel.getEn(),0d)
+ minInv * weights.getOrDefault(KpiLib.MinInventoryLevel.getEn(),0d) // + minInv * weights.getOrDefault(KpiLib.MinInventoryLevel.getEn(),0d)
+ targetInv * weights.getOrDefault(KpiLib.TargetInvLevel.getEn(),0d) // + targetInv * weights.getOrDefault(KpiLib.TargetInvLevel.getEn(),0d)
+ capacity * weights.getOrDefault(KpiLib.UnitCapacity.getEn(),0d) // + capacity * weights.getOrDefault(KpiLib.UnitCapacity.getEn(),0d)
+ supplyTarget * weights.getOrDefault(KpiLib.SupplyTarget.getEn(),0d) // + supplyTarget * weights.getOrDefault(KpiLib.SupplyTarget.getEn(),0d)
+ minSupply * weights.getOrDefault(KpiLib.MinSupply.getEn(),0d) // + minSupply * weights.getOrDefault(KpiLib.MinSupply.getEn(),0d)
+ maxSupply * weights.getOrDefault(KpiLib.MaxSupply.getEn(),0d) // + maxSupply * weights.getOrDefault(KpiLib.MaxSupply.getEn(),0d)
- salesPriority * weights.getOrDefault(KpiLib.SalesDemandPriority.getEn(),0d); // - salesPriority * weights.getOrDefault(KpiLib.SalesDemandPriority.getEn(),0d);
//
writeLog(" 加权总惩罚: %.2f", totalPenalty); // writeLog(" 总kpi: %.2f", totalPenalty);
for (Map.Entry<String, Double> entry : weights.entrySet()) { for (Map.Entry<String, Double> entry : weights.entrySet()) {
String key = entry.getKey(); String key = entry.getKey();
Double weight = entry.getValue(); Double weight = entry.getValue();
KpiLib kpi = KpiLib.ofEn(key); KpiLib kpi = KpiLib.ofEn(key);
double val= model.getKpi(key).solutionValue(); double val= model.getKpi(key).solutionValue();
writeLog(" %s: %.2f (权重%.0f × %.2f)", writeLog(" %s: %.2f (%.0f × %.2f)",
kpi.getCn(),val * weight, val, weight); kpi.getCn(),val * weight, val, weight);
} }
......
...@@ -26,6 +26,8 @@ public class KpiResult { ...@@ -26,6 +26,8 @@ public class KpiResult {
public Double weight; public Double weight;
/** 加权惩罚;权重为空时为 null */ /** 加权惩罚;权重为空时为 null */
public Double penalty; public Double penalty;
/** 得分贡献 = rawValue × weight(收益项为正, 惩罚项为负);无权重时为 null */
public Double contribution;
/** 是否为收益项 (越大越好, 正系数) */ /** 是否为收益项 (越大越好, 正系数) */
public boolean isBenefit; public boolean isBenefit;
} }
...@@ -44,6 +46,7 @@ public class KpiResult { ...@@ -44,6 +46,7 @@ public class KpiResult {
e.weight = weight; e.weight = weight;
e.isBenefit = isBenefit; e.isBenefit = isBenefit;
e.penalty = weight == null ? null : (isBenefit ? -rawValue * weight : rawValue * weight); e.penalty = weight == null ? null : (isBenefit ? -rawValue * weight : rawValue * weight);
e.contribution = weight == null ? null : (isBenefit ? rawValue * weight : -rawValue * weight);
entries.add(e); entries.add(e);
} }
} }
...@@ -33,6 +33,8 @@ public class OptimizationResult { ...@@ -33,6 +33,8 @@ public class OptimizationResult {
private final List<SalesDemandResult> salesDemands = new ArrayList<>(); private final List<SalesDemandResult> salesDemands = new ArrayList<>();
private List<SalesDemandResult> saleDemandSummaries = new ArrayList<>(); private List<SalesDemandResult> saleDemandSummaries = new ArrayList<>();
private List<RiskItem> saleDemandReasons = new ArrayList<>();
private final List<PispipResult> pispips = new ArrayList<>(); private final List<PispipResult> pispips = new ArrayList<>();
...@@ -76,8 +78,12 @@ public class OptimizationResult { ...@@ -76,8 +78,12 @@ public class OptimizationResult {
public List<SalesDemandResult> getSaleSummarieDemands() { return saleDemandSummaries; } public List<SalesDemandResult> getSaleSummarieDemands() { return saleDemandSummaries; }
public List<RiskItem> getSaleDemandReasons() { return saleDemandReasons; }
public void setSaleSummarieDemands(List<SalesDemandResult> v) { saleDemandSummaries=v; } public void setSaleSummarieDemands(List<SalesDemandResult> v) { saleDemandSummaries=v; }
public void setSaleDemandReasons(List<RiskItem> v) { saleDemandReasons=v; }
public List<PispipResult> getPispips() { return pispips; } public List<PispipResult> getPispips() { return pispips; }
......
...@@ -2,6 +2,7 @@ package com.aps.macroplanner.output.dto; ...@@ -2,6 +2,7 @@ package com.aps.macroplanner.output.dto;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/** /**
* 作者:佟礼 * 作者:佟礼
...@@ -12,16 +13,27 @@ import com.fasterxml.jackson.annotation.JsonProperty; ...@@ -12,16 +13,27 @@ import com.fasterxml.jackson.annotation.JsonProperty;
* *
* <p>level 取值: "高" / "低"。</p> * <p>level 取值: "高" / "低"。</p>
*/ */
@Data
public class RiskItem { public class RiskItem {
private final String level;
private final String description;
@JsonCreator private String salesDemandId;
public RiskItem(@JsonProperty("level") String level, @JsonProperty("description") String description) {
private Integer periodIndex;
private Integer type;
private String level;
private String title;
private String description;
public RiskItem()
{}
public RiskItem(String salesDemandId,int periodIndex,int type,String level,String title, String description) {
this.salesDemandId=salesDemandId;
this.periodIndex=periodIndex;
this.type=type;
this.level = level; this.level = level;
this.title = title;
this.description = description; this.description = description;
} }
public String getLevel() { return level; }
public String getDescription() { return description; }
} }
...@@ -39,9 +39,9 @@ public class SalesDemandResult { ...@@ -39,9 +39,9 @@ public class SalesDemandResult {
private Long categoryId; private Long categoryId;
/** 未完成原因分析 (仅当 unmetQty > 0 时有内容) */ /** 未完成原因分析 (仅当 unmetQty > 0 时有内容) */
private final List<String> unmetReasons = new ArrayList<>(); private List<RiskItem> unmetReasons = new ArrayList<>();
/** 风险分析 (即使满足也可能存在的供应链风险) */ /** 风险分析 (即使满足也可能存在的供应链风险) */
private final List<RiskItem> risks = new ArrayList<>(); private List<RiskItem> risks = new ArrayList<>();
// ==================== Getters / Setters ==================== // ==================== Getters / Setters ====================
...@@ -88,8 +88,10 @@ public class SalesDemandResult { ...@@ -88,8 +88,10 @@ public class SalesDemandResult {
public double getPriority() { return priority; } public double getPriority() { return priority; }
public void setPriority(double v) { this.priority = v; } public void setPriority(double v) { this.priority = v; }
public List<String> getUnmetReasons() { return unmetReasons; } public List<RiskItem> getUnmetReasons() { return unmetReasons; }
public List<RiskItem> getRisks() { return risks; } public List<RiskItem> getRisks() { return risks; }
public void setUnmetReasons(List<RiskItem> v) { unmetReasons=v; }
public void setRisks(List<RiskItem> v) { risks=v; }
} }
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