增加原料添加量限制
This commit is contained in:
@@ -33,10 +33,11 @@ func NewRecipeGenerateManager(ctx context.Context) RecipeGenerateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// 内部虚拟填充料的名称,用于线性规划计算,不应出现在最终配方中。
|
// internalFillerRawMaterialName 是内部虚拟填充料的名称。
|
||||||
|
// 该填充料用于线性规划计算,确保总比例为100%,但不会出现在最终配方中。
|
||||||
internalFillerRawMaterialName = "内部填充料_InternalFiller"
|
internalFillerRawMaterialName = "内部填充料_InternalFiller"
|
||||||
// 内部虚拟填充营养素的ID,用于关联填充料,确保其不与实际营养素冲突。
|
// internalFillerNutrientID 是内部虚拟填充营养素的ID。
|
||||||
// 使用 math.MaxUint32 作为一个极大的、不可能与实际ID冲突的值。
|
// 使用 math.MaxUint32 作为一个极大的、不可能与实际ID冲突的值,用于关联填充料。
|
||||||
internalFillerNutrientID = math.MaxUint32
|
internalFillerNutrientID = math.MaxUint32
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,13 +51,14 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType
|
|||||||
return nil, errors.New("无法生成配方:猪类型未设置营养需求")
|
return nil, errors.New("无法生成配方:猪类型未设置营养需求")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 剔除无用原料
|
// 收集猪类型所有有需求的营养素ID (包括min_requirement或max_requirement不为0的)。
|
||||||
// 收集猪类型所有有需求的营养素ID (包括min_requirement或max_requirement不为0的)
|
// 用于后续过滤掉完全不相关的原料。
|
||||||
requiredNutrientIDs := make(map[uint32]bool)
|
requiredNutrientIDs := make(map[uint32]bool)
|
||||||
for _, req := range pigType.PigNutrientRequirements {
|
for _, req := range pigType.PigNutrientRequirements {
|
||||||
requiredNutrientIDs[req.NutrientID] = true
|
requiredNutrientIDs[req.NutrientID] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 过滤掉那些不包含猪类型任何所需营养素的原料。
|
||||||
var filteredMaterials []models.RawMaterial
|
var filteredMaterials []models.RawMaterial
|
||||||
for _, mat := range materials {
|
for _, mat := range materials {
|
||||||
hasRelevantNutrient := false
|
hasRelevantNutrient := false
|
||||||
@@ -100,11 +102,11 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType
|
|||||||
// 2. 准备数据结构
|
// 2. 准备数据结构
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
// 映射: 为了快速查找原料的营养含量 [RawMaterialID][NutrientID] => Value
|
// materialNutrients 映射: 为了快速查找原料的营养含量 [RawMaterialID][NutrientID] => Value
|
||||||
materialNutrients := make(map[uint32]map[uint32]float64)
|
materialNutrients := make(map[uint32]map[uint32]float64)
|
||||||
// 映射: 原料ID到矩阵列索引的映射 (前 N 列对应 N 种原料)
|
// materialIndex 映射: 原料ID到矩阵列索引的映射 (前 N 列对应 N 种原料)
|
||||||
materialIndex := make(map[uint32]int)
|
materialIndex := make(map[uint32]int)
|
||||||
// 列表: 记录原料ID以便结果回溯
|
// materialIDs 列表: 记录原料ID以便结果回溯
|
||||||
materialIDs := make([]uint32, len(materials))
|
materialIDs := make([]uint32, len(materials))
|
||||||
|
|
||||||
for i, m := range materials {
|
for i, m := range materials {
|
||||||
@@ -117,16 +119,13 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 识别约束数量
|
// nutrientConstraints 存储营养素的下限和上限约束信息。
|
||||||
// 约束 1: 总重量 = 1 (100%)
|
type nutrientConstraintInfo struct {
|
||||||
// 约束 2..N: 营养素下限 (Min)
|
|
||||||
// 约束 N..M: 营养素上限 (Max, 仅当 Max > 0 时)
|
|
||||||
type constraintInfo struct {
|
|
||||||
isMax bool // true=上限约束(<=), false=下限约束(>=)
|
isMax bool // true=上限约束(<=), false=下限约束(>=)
|
||||||
nutrientID uint32
|
nutrientID uint32
|
||||||
limit float64
|
limit float64
|
||||||
}
|
}
|
||||||
var constraints []constraintInfo
|
var nutrientConstraints []nutrientConstraintInfo
|
||||||
|
|
||||||
// 添加营养约束
|
// 添加营养约束
|
||||||
for _, req := range pigType.PigNutrientRequirements {
|
for _, req := range pigType.PigNutrientRequirements {
|
||||||
@@ -135,22 +134,22 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType
|
|||||||
continue
|
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{
|
nutrientConstraints = append(nutrientConstraints, nutrientConstraintInfo{
|
||||||
isMax: false,
|
isMax: false,
|
||||||
nutrientID: req.NutrientID,
|
nutrientID: req.NutrientID,
|
||||||
limit: float64(req.MinRequirement),
|
limit: float64(req.MinRequirement),
|
||||||
})
|
})
|
||||||
|
|
||||||
// 上限约束 (Value <= Max)
|
// 添加上限约束 (Value <= Max)
|
||||||
// 逻辑: Sum(Mat * x) <= Max -> Sum(Mat * x) + slack = Max
|
// 逻辑: Sum(Mat * x) <= Max -> Sum(Mat * x) + slack = Max
|
||||||
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("营养素 %d 的需求配置无效: 最小需求 (%f) 大于最大需求 (%f)", req.NutrientID, req.MinRequirement, req.MaxRequirement)
|
return nil, fmt.Errorf("营养素 %d 的需求配置无效: 最小需求 (%f) 大于最大需求 (%f)", req.NutrientID, req.MinRequirement, req.MaxRequirement)
|
||||||
}
|
}
|
||||||
constraints = append(constraints, constraintInfo{
|
nutrientConstraints = append(nutrientConstraints, nutrientConstraintInfo{
|
||||||
isMax: true,
|
isMax: true,
|
||||||
nutrientID: req.NutrientID,
|
nutrientID: req.NutrientID,
|
||||||
limit: float64(req.MaxRequirement),
|
limit: float64(req.MaxRequirement),
|
||||||
@@ -158,19 +157,49 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// maxAdditionConstraints 存储每个原料的最大添加比例约束 (x_i <= limit)。
|
||||||
|
type maxAdditionConstraintInfo struct {
|
||||||
|
materialColIndex int // 原料在 A 矩阵中的列索引
|
||||||
|
limit float64
|
||||||
|
}
|
||||||
|
var maxAdditionConstraints []maxAdditionConstraintInfo
|
||||||
|
|
||||||
|
// 遍历所有原料,包括填充料,添加 MaxAdditionRatio 约束
|
||||||
|
for _, mat := range materials {
|
||||||
|
// 填充料不应受 MaxAdditionRatio 限制
|
||||||
|
if mat.ID == fillerRawMaterial.ID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有当 MaxAdditionRatio >= 0 时才添加约束。
|
||||||
|
// 如果 MaxAdditionRatio 为 0,表示该原料最大添加比例为 0%,即不能添加。
|
||||||
|
// 如果 MaxAdditionRatio 为正数,则为实际限制。
|
||||||
|
if mat.MaxAdditionRatio >= 0 {
|
||||||
|
materialColIndex, ok := materialIndex[mat.ID]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("内部错误:未找到原料 %d (%s) 的列索引", mat.ID, mat.Name)
|
||||||
|
}
|
||||||
|
maxAdditionConstraints = append(maxAdditionConstraints, maxAdditionConstraintInfo{
|
||||||
|
materialColIndex: materialColIndex,
|
||||||
|
limit: float64(mat.MaxAdditionRatio),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
// 3. 构建线性规划矩阵 (Ax = b) 和 目标函数 (c)
|
// 3. 构建线性规划矩阵 (Ax = b) 和 目标函数 (c)
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
// 变量总数 = 原料数量 + 松弛变量数量
|
|
||||||
// 松弛变量数量 = 约束数量 (每个不等式约束需要一个松弛变量)
|
|
||||||
// 注意:总量约束 (Sum=1) 是等式,理论上不需要松弛变量,但在单纯形法标准型中通常处理为 Ax=b
|
|
||||||
numMaterials := len(materials) // 此时已包含填充料
|
numMaterials := len(materials) // 此时已包含填充料
|
||||||
numSlack := len(constraints)
|
numNutrientConstraints := len(nutrientConstraints)
|
||||||
|
numMaxAdditionConstraints := len(maxAdditionConstraints)
|
||||||
|
|
||||||
|
// 松弛变量数量 = 营养约束数量 + 最大添加比例约束数量
|
||||||
|
numSlack := numNutrientConstraints + numMaxAdditionConstraints
|
||||||
numCols := numMaterials + numSlack
|
numCols := numMaterials + numSlack
|
||||||
|
|
||||||
// 行数 = 1 (总量约束) + 营养约束数量
|
// 行数 = 1 (总量约束) + 营养约束数量 + 最大添加比例约束数量
|
||||||
numRows := 1 + len(constraints)
|
numRows := 1 + numNutrientConstraints + numMaxAdditionConstraints
|
||||||
|
|
||||||
// A: 约束系数矩阵
|
// A: 约束系数矩阵
|
||||||
A := mat.NewDense(numRows, numCols, nil)
|
A := mat.NewDense(numRows, numCols, nil)
|
||||||
@@ -179,34 +208,38 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType
|
|||||||
// c: 成本向量 (目标函数系数)
|
// c: 成本向量 (目标函数系数)
|
||||||
c := make([]float64, numCols)
|
c := make([]float64, numCols)
|
||||||
|
|
||||||
// --- 填充 c (成本) ---
|
// 填充 c (成本)
|
||||||
for i, m := range materials {
|
for i, m := range materials {
|
||||||
c[i] = float64(m.ReferencePrice)
|
c[i] = float64(m.ReferencePrice)
|
||||||
}
|
}
|
||||||
// 松弛变量的成本为 0,Go 默认初始化为 0,无需操作
|
// 松弛变量的成本为 0,Go 默认初始化为 0,无需操作
|
||||||
|
|
||||||
// --- 填充 Row 0: 总量约束 (Sum(x) = 1) ---
|
// 填充 Row 0: 总量约束 (Sum(x) = 1)
|
||||||
// 系数: 所有原料对应列为 1,松弛变量列为 0
|
// 系数: 所有原料对应列为 1,松弛变量列为 0
|
||||||
for j := 0; j < numMaterials; j++ {
|
for j := 0; j < numMaterials; j++ {
|
||||||
A.Set(0, j, 1.0)
|
A.Set(0, j, 1.0)
|
||||||
}
|
}
|
||||||
b[0] = 1.0
|
b[0] = 1.0
|
||||||
|
|
||||||
// --- 填充营养约束行 ---
|
// currentConstraintRowIndex 记录当前正在填充的约束行索引,从1开始(0行被总量约束占用)。
|
||||||
for i, cons := range constraints {
|
currentConstraintRowIndex := 1
|
||||||
rowIndex := i + 1 // 0行被总量约束占用,所以从1开始
|
|
||||||
slackColIndex := numMaterials + i // 松弛变量列紧跟在原料列之后
|
// 填充营养约束行
|
||||||
|
for i, cons := range nutrientConstraints {
|
||||||
|
rowIndex := currentConstraintRowIndex + i
|
||||||
|
// 营养约束的松弛变量列紧跟在原料列之后
|
||||||
|
slackColIndex := numMaterials + i
|
||||||
|
|
||||||
b[rowIndex] = cons.limit
|
b[rowIndex] = cons.limit
|
||||||
|
|
||||||
// 1. 设置原料系数
|
// 设置原料系数
|
||||||
for j, m := range materials {
|
for j, m := range materials {
|
||||||
// 获取该原料这种营养素的含量,如果没有则为0
|
// 获取该原料这种营养素的含量,如果没有则为0
|
||||||
val := materialNutrients[m.ID][cons.nutrientID]
|
val := materialNutrients[m.ID][cons.nutrientID]
|
||||||
A.Set(rowIndex, j, val)
|
A.Set(rowIndex, j, val)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 设置松弛变量系数
|
// 设置松弛变量系数
|
||||||
// 如果是下限 (>=): Sum - s = Limit => s系数为 -1
|
// 如果是下限 (>=): Sum - s = Limit => s系数为 -1
|
||||||
// 如果是上限 (<=): Sum + s = Limit => s系数为 +1
|
// 如果是上限 (<=): Sum + s = Limit => s系数为 +1
|
||||||
if cons.isMax {
|
if cons.isMax {
|
||||||
@@ -215,6 +248,19 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType
|
|||||||
A.Set(rowIndex, slackColIndex, -1.0)
|
A.Set(rowIndex, slackColIndex, -1.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
currentConstraintRowIndex += numNutrientConstraints // 推进当前约束行索引
|
||||||
|
|
||||||
|
// 填充 MaxAdditionRatio 约束行
|
||||||
|
for i, cons := range maxAdditionConstraints {
|
||||||
|
rowIndex := currentConstraintRowIndex + i
|
||||||
|
// MaxAdditionRatio 约束的松弛变量列在营养约束的松弛变量之后
|
||||||
|
slackColIndex := numMaterials + numNutrientConstraints + i
|
||||||
|
|
||||||
|
// 约束形式: x_j + s_k = Limit_j (其中 x_j 是原料 j 的比例,s_k 是松弛变量)
|
||||||
|
A.Set(rowIndex, cons.materialColIndex, 1.0) // 原料本身的系数
|
||||||
|
A.Set(rowIndex, slackColIndex, 1.0) // 松弛变量的系数
|
||||||
|
b[rowIndex] = cons.limit
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
// 4. 执行单纯形法求解
|
// 4. 执行单纯形法求解
|
||||||
@@ -225,7 +271,7 @@ 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("计算错误:解无界 (可能数据配置有误,例如某个营养素没有上限约束且成本为负)")
|
||||||
@@ -254,15 +300,16 @@ func (r *recipeGenerateManagerImpl) GenerateRecipe(ctx context.Context, pigType
|
|||||||
// 遍历原料部分的解 (前 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 {
|
if materialIDs[i] == fillerRawMaterial.ID {
|
||||||
continue // 跳过填充料,不将其加入最终配方
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
proportion := x[i]
|
proportion := x[i]
|
||||||
|
|
||||||
// 忽略极小值 (浮点数误差)
|
// 忽略极小值 (浮点数误差)。
|
||||||
if proportion < 1e-4 { // 万分之一,即0.01%
|
// 调整过滤阈值到万分之一 (0.01%),即小于0.0001的比例将被忽略。
|
||||||
|
if proportion < 1e-4 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,12 +318,14 @@ 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],
|
||||||
|
// 比例: float64 -> float32
|
||||||
Percentage: float32(proportion),
|
Percentage: float32(proportion),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 二次校验: 确保实际原料总量不超过 100% (允许小于100%因为填充料被移除)
|
// 二次校验: 确保实际原料总量不超过 100% (允许小于100%因为填充料被移除)。
|
||||||
if totalPercentage > 1.0+1e-3 { // 允许略微超过100%的浮点误差,但不能显著超过
|
// 允许略微超过100%的浮点误差,但不能显著超过。
|
||||||
|
if totalPercentage > 1.0+1e-3 {
|
||||||
return nil, fmt.Errorf("计算结果异常:实际原料总量超过 100%% (计算值: %.4f),请检查算法或数据配置", totalPercentage)
|
return nil, fmt.Errorf("计算结果异常:实际原料总量超过 100%% (计算值: %.4f),请检查算法或数据配置", totalPercentage)
|
||||||
}
|
}
|
||||||
// 如果 totalPercentage 小于 1.0,说明填充料被使用,这是符合预期的。
|
// 如果 totalPercentage 小于 1.0,说明填充料被使用,这是符合预期的。
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type RawMaterial struct {
|
|||||||
Name string `gorm:"size:100;not null;comment:原料名称"`
|
Name string `gorm:"size:100;not null;comment:原料名称"`
|
||||||
Description string `gorm:"size:255;comment:描述"`
|
Description string `gorm:"size:255;comment:描述"`
|
||||||
ReferencePrice float32 `gorm:"comment:参考价格(kg/元)"`
|
ReferencePrice float32 `gorm:"comment:参考价格(kg/元)"`
|
||||||
|
MaxAdditionRatio float32 `gorm:"comment:该物质最大添加比例"`
|
||||||
// RawMaterialNutrients 关联此原料的所有营养素含量信息
|
// RawMaterialNutrients 关联此原料的所有营养素含量信息
|
||||||
RawMaterialNutrients []RawMaterialNutrient `gorm:"foreignKey:RawMaterialID"`
|
RawMaterialNutrients []RawMaterialNutrient `gorm:"foreignKey:RawMaterialID"`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user