修复逻辑错误
This commit is contained in:
@@ -32,16 +32,70 @@ func NewRecipeGenerateManager(ctx context.Context) RecipeGenerateManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// 内部虚拟填充料的名称,用于线性规划计算,不应出现在最终配方中。
|
||||||
|
internalFillerRawMaterialName = "内部填充料_InternalFiller"
|
||||||
|
// 内部虚拟填充营养素的ID,用于关联填充料,确保其不与实际营养素冲突。
|
||||||
|
// 使用 math.MaxUint32 作为一个极大的、不可能与实际ID冲突的值。
|
||||||
|
internalFillerNutrientID = math.MaxUint32
|
||||||
|
)
|
||||||
|
|
||||||
// GenerateRecipe 根据猪的营养需求和可用原料,使用线性规划计算出成本最低的饲料配方。
|
// GenerateRecipe 根据猪的营养需求和可用原料,使用线性规划计算出成本最低的饲料配方。
|
||||||
func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType models.PigType, materials []models.RawMaterial) (*models.Recipe, error) {
|
func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType models.PigType, materials []models.RawMaterial) (*models.Recipe, error) {
|
||||||
// 1. 基础校验
|
// 1. 基础校验
|
||||||
if len(materials) == 0 {
|
if len(materials) == 0 {
|
||||||
return nil, errors.New("cannot generate recipe: no raw materials provided")
|
return nil, errors.New("无法生成配方:未提供任何原料")
|
||||||
}
|
}
|
||||||
if len(pigType.PigNutrientRequirements) == 0 {
|
if len(pigType.PigNutrientRequirements) == 0 {
|
||||||
return nil, errors.New("cannot generate recipe: pig type has no nutrient requirements")
|
return nil, errors.New("无法生成配方:猪类型未设置营养需求")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 剔除无用原料
|
||||||
|
// 收集猪类型所有有需求的营养素ID (包括min_requirement或max_requirement不为0的)
|
||||||
|
requiredNutrientIDs := make(map[uint32]bool)
|
||||||
|
for _, req := range pigType.PigNutrientRequirements {
|
||||||
|
requiredNutrientIDs[req.NutrientID] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var filteredMaterials []models.RawMaterial
|
||||||
|
for _, mat := range materials {
|
||||||
|
hasRelevantNutrient := false
|
||||||
|
for _, matNut := range mat.RawMaterialNutrients {
|
||||||
|
// 检查原料是否包含猪类型所需的任何营养素
|
||||||
|
if requiredNutrientIDs[matNut.NutrientID] {
|
||||||
|
hasRelevantNutrient = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果原料包含至少一个猪类型需求的营养素,则保留
|
||||||
|
if hasRelevantNutrient {
|
||||||
|
filteredMaterials = append(filteredMaterials, mat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
materials = filteredMaterials // 使用过滤后的原料列表
|
||||||
|
|
||||||
|
if len(materials) == 0 {
|
||||||
|
return nil, errors.New("无法生成配方:所有提供的原料都不包含猪类型所需的任何营养素,请检查原料配置或猪类型营养需求")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建一个虚拟的、价格为0、不含任何实际营养素的填充料。
|
||||||
|
// 其唯一目的是在LP求解中作为“凑数”的选项,确保总比例为100%,且不影响实际配方成本。
|
||||||
|
fillerRawMaterial := models.RawMaterial{
|
||||||
|
Model: models.Model{
|
||||||
|
ID: math.MaxUint32 - 1, // 使用一个极大的、不可能与实际原料ID冲突的值
|
||||||
|
},
|
||||||
|
Name: internalFillerRawMaterialName,
|
||||||
|
Description: "内部虚拟填充料,用于线性规划凑足100%比例,不含实际营养,价格为0。",
|
||||||
|
ReferencePrice: 0.0, // 价格为0,确保LP优先选择它来凑数
|
||||||
|
RawMaterialNutrients: []models.RawMaterialNutrient{
|
||||||
|
{
|
||||||
|
NutrientID: internalFillerNutrientID, // 关联一个虚拟营养素,确保其在LP中被识别,但其含量为0
|
||||||
|
Value: 0.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
materials = append(materials, fillerRawMaterial) // 将填充料添加到原料列表中
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
// 2. 准备数据结构
|
// 2. 准备数据结构
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
@@ -76,6 +130,11 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType
|
|||||||
|
|
||||||
// 添加营养约束
|
// 添加营养约束
|
||||||
for _, req := range pigType.PigNutrientRequirements {
|
for _, req := range pigType.PigNutrientRequirements {
|
||||||
|
// 排除内部虚拟填充营养素的约束,因为它不应有实际需求
|
||||||
|
if req.NutrientID == internalFillerNutrientID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// 下限约束 (Value >= Min)
|
// 下限约束 (Value >= Min)
|
||||||
// 逻辑: Sum(Mat * x) >= Min -> Sum(Mat * x) - slack = Min
|
// 逻辑: Sum(Mat * x) >= Min -> Sum(Mat * x) - slack = Min
|
||||||
constraints = append(constraints, constraintInfo{
|
constraints = append(constraints, constraintInfo{
|
||||||
@@ -89,7 +148,7 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType
|
|||||||
if req.MaxRequirement > 0 {
|
if req.MaxRequirement > 0 {
|
||||||
// 简单的校验,如果 Min > Max 则是逻辑矛盾,直接报错
|
// 简单的校验,如果 Min > Max 则是逻辑矛盾,直接报错
|
||||||
if req.MinRequirement > req.MaxRequirement {
|
if req.MinRequirement > req.MaxRequirement {
|
||||||
return nil, fmt.Errorf("invalid requirement for nutrient %d: min > max", req.NutrientID)
|
return nil, fmt.Errorf("营养素 %d 的需求配置无效: 最小需求 (%f) 大于最大需求 (%f)", req.NutrientID, req.MinRequirement, req.MaxRequirement)
|
||||||
}
|
}
|
||||||
constraints = append(constraints, constraintInfo{
|
constraints = append(constraints, constraintInfo{
|
||||||
isMax: true,
|
isMax: true,
|
||||||
@@ -106,7 +165,7 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType
|
|||||||
// 变量总数 = 原料数量 + 松弛变量数量
|
// 变量总数 = 原料数量 + 松弛变量数量
|
||||||
// 松弛变量数量 = 约束数量 (每个不等式约束需要一个松弛变量)
|
// 松弛变量数量 = 约束数量 (每个不等式约束需要一个松弛变量)
|
||||||
// 注意:总量约束 (Sum=1) 是等式,理论上不需要松弛变量,但在单纯形法标准型中通常处理为 Ax=b
|
// 注意:总量约束 (Sum=1) 是等式,理论上不需要松弛变量,但在单纯形法标准型中通常处理为 Ax=b
|
||||||
numMaterials := len(materials)
|
numMaterials := len(materials) // 此时已包含填充料
|
||||||
numSlack := len(constraints)
|
numSlack := len(constraints)
|
||||||
numCols := numMaterials + numSlack
|
numCols := numMaterials + numSlack
|
||||||
|
|
||||||
@@ -166,10 +225,10 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, lp.ErrInfeasible) {
|
if errors.Is(err, lp.ErrInfeasible) {
|
||||||
return nil, errors.New("无法生成配方:根据提供的原料,无法满足所有营养需求 (无可行解)")
|
return nil, errors.New("无法生成配方:根据提供的原料,无法满足所有营养需求 (无可行解),请检查原料或营养需求配置")
|
||||||
}
|
}
|
||||||
if errors.Is(err, lp.ErrUnbounded) {
|
if errors.Is(err, lp.ErrUnbounded) {
|
||||||
return nil, errors.New("计算错误:解无界 (可能数据配置有误)")
|
return nil, errors.New("计算错误:解无界 (可能数据配置有误,例如某个营养素没有上限约束且成本为负)")
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("配方计算失败: %w", err)
|
return nil, fmt.Errorf("配方计算失败: %w", err)
|
||||||
}
|
}
|
||||||
@@ -178,19 +237,32 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType
|
|||||||
// 5. 结果解析与构建
|
// 5. 结果解析与构建
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
|
// 统计实际原料数量(排除填充料)
|
||||||
|
actualMaterialCount := 0
|
||||||
|
for _, m := range materials {
|
||||||
|
if m.ID != fillerRawMaterial.ID {
|
||||||
|
actualMaterialCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
recipe := &models.Recipe{
|
recipe := &models.Recipe{
|
||||||
Name: fmt.Sprintf("%s-%s - 自动计算配方", pigType.Breed.Name, pigType.AgeStage.Name),
|
Name: fmt.Sprintf("%s-%s - 自动计算配方", pigType.Breed.Name, pigType.AgeStage.Name),
|
||||||
Description: fmt.Sprintf("基于 %d 种原料计算的最优成本配方。计算时预估成本: %.2f元/kg", len(materials), optVal),
|
Description: fmt.Sprintf("基于 %d 种原料计算的最优成本配方。计算时预估成本: %.2f元/kg", actualMaterialCount, optVal),
|
||||||
RecipeIngredients: []models.RecipeIngredient{},
|
RecipeIngredients: []models.RecipeIngredient{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 遍历原料部分的解 (前 numMaterials 个变量)
|
// 遍历原料部分的解 (前 numMaterials 个变量)
|
||||||
totalPercentage := 0.0
|
totalPercentage := 0.0
|
||||||
for i := 0; i < numMaterials; i++ {
|
for i := 0; i < numMaterials; i++ {
|
||||||
|
// 排除内部虚拟填充料 ---
|
||||||
|
if materialIDs[i] == fillerRawMaterial.ID {
|
||||||
|
continue // 跳过填充料,不将其加入最终配方
|
||||||
|
}
|
||||||
|
|
||||||
proportion := x[i]
|
proportion := x[i]
|
||||||
|
|
||||||
// 忽略极小值 (浮点数误差)
|
// 忽略极小值 (浮点数误差)
|
||||||
if proportion < 1e-6 {
|
if proportion < 1e-4 { // 万分之一,即0.01%
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,16 +271,15 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType
|
|||||||
|
|
||||||
recipe.RecipeIngredients = append(recipe.RecipeIngredients, models.RecipeIngredient{
|
recipe.RecipeIngredients = append(recipe.RecipeIngredients, models.RecipeIngredient{
|
||||||
RawMaterialID: materialIDs[i],
|
RawMaterialID: materialIDs[i],
|
||||||
// 数据库可能需要 RawMaterial 对象,这里只填ID,由调用方或ORM处理加载
|
|
||||||
// 比例: float64 -> float32
|
|
||||||
Percentage: float32(proportion),
|
Percentage: float32(proportion),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 二次校验: 确保总量约为 1
|
// 二次校验: 确保实际原料总量不超过 100% (允许小于100%因为填充料被移除)
|
||||||
if math.Abs(totalPercentage-1.0) > 1e-3 {
|
if totalPercentage > 1.0+1e-3 { // 允许略微超过100%的浮点误差,但不能显著超过
|
||||||
return nil, fmt.Errorf("计算结果异常:原料总量不为 100%% (计算值: %.4f)", totalPercentage)
|
return nil, fmt.Errorf("计算结果异常:实际原料总量超过 100%% (计算值: %.4f),请检查算法或数据配置", totalPercentage)
|
||||||
}
|
}
|
||||||
|
// 如果 totalPercentage 小于 1.0,说明填充料被使用,这是符合预期的。
|
||||||
|
|
||||||
return recipe, nil
|
return recipe, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user