diff --git a/feed_formula_calculator.py b/feed_formula_calculator.py index ae4e6d5..8372d79 100644 --- a/feed_formula_calculator.py +++ b/feed_formula_calculator.py @@ -1,36 +1,240 @@ -import optimize_feed +import pulp +import pandas as pd +from tabulate import tabulate +import os -# 示例调用 -if __name__ == "__main__": - # 原料数据(加工后,单位:%或MJ/kg,价格元/吨) - ingredients = { - '构树叶': {'蛋白': 15, '能量': 9, '纤维': 10, '赖氨酸': 0.5, '蛋氨酸': 0.1, '钙': 1.5, '磷': 0.2, '价格': 600}, - '玉米胚芽': {'蛋白': 11, '能量': 13, '纤维': 7, '赖氨酸': 0.4, '蛋氨酸': 0.2, '钙': 0.1, '磷': 0.3, - '价格': 1700}, - '麦麸': {'蛋白': 15, '能量': 10, '纤维': 12, '赖氨酸': 0.4, '蛋氨酸': 0.2, '钙': 0.1, '磷': 1.0, '价格': 1300}, - '米糠': {'蛋白': 13, '能量': 11, '纤维': 10, '赖氨酸': 0.4, '蛋氨酸': 0.2, '钙': 0.1, '磷': 1.5, '价格': 1100}, - '牧草': {'蛋白': 17, '能量': 8.5, '纤维': 20, '赖氨酸': 0.6, '蛋氨酸': 0.2, '钙': 1.2, '磷': 0.3, '价格': 800}, - '玉米': {'蛋白': 9, '能量': 13.5, '纤维': 2.5, '赖氨酸': 0.3, '蛋氨酸': 0.17, '钙': 0.02, '磷': 0.3, - '价格': 2367}, - '豆粕': {'蛋白': 46, '能量': 10.5, '纤维': 4, '赖氨酸': 2.8, '蛋氨酸': 0.6, '钙': 0.3, '磷': 0.7, '价格': 3300}, - '植物油': {'蛋白': 0, '能量': 36, '纤维': 0, '赖氨酸': 0, '蛋氨酸': 0, '钙': 0, '磷': 0, '价格': 7500}, - '赖氨酸': {'蛋白': 78, '能量': 0, '纤维': 0, '赖氨酸': 78, '蛋氨酸': 0, '钙': 0, '磷': 0, '价格': 15000}, - '蛋氨酸': {'蛋白': 99, '能量': 0, '纤维': 0, '赖氨酸': 0, '蛋氨酸': 99, '钙': 0, '磷': 0, '价格': 25000}, - '预混料': {'蛋白': 0, '能量': 0, '纤维': 0, '赖氨酸': 0, '蛋氨酸': 0, '钙': 10, '磷': 5, '价格': 3000} + +def calculate_nutrients_from_formula(formula_proportions, ingredients): + """ + 计算市面配方营养总量,作为需求校准 + :param formula_proportions: 配方比例字典,如 {'玉米': 0.59, '豆粕': 0.15} + :param ingredients: 原料数据字典,含营养值/价格 + :return: 营养总量字典,如 {'蛋白': 17.76, '能量': 11.56} + """ + nutrients = list(set(key for ing in ingredients.values() for key in ing if key not in ['价格'])) + total_nutrients = {} + for nut in nutrients: + total = sum(formula_proportions.get(i, 0) * ingredients.get(i, {}).get(nut, 0) for i in formula_proportions) + total_nutrients[nut] = round(total, 2) + return total_nutrients + + +def calculate_cost_from_formula(formula_proportions, ingredients): + """ + 计算市面配方成本,作为参考 + :param formula_proportions: 配方比例字典,如 {'玉米': 0.59} + :param ingredients: 原料数据字典,含价格 + :return: 成本字典,含总价格和一斤价格 + """ + total_price = sum( + formula_proportions.get(i, 0) * ingredients.get(i, {}).get('价格', 0) for i in formula_proportions) + return { + '总价格': round(total_price, 2), + '一斤饲料价格': round(total_price / 2000, 2) # 吨=2000斤 } + +def optimize_feed(requirements, ingredients, result_fields, optimization_type='min'): + """ + 优化饲料配方,计算最低/最高目标值的原料比例 + :param requirements: 猪营养需求字典,如 {'蛋白_下限': 17, '能量_下限': 12.5, '纤维_上限': 6} + :param ingredients: 原料数据字典,如 {'构树叶_发酵': {'蛋白': 15, '能量': 9, '纤维': 15, '价格': 600}} + :param result_fields: 结果字段列表,如 ['比例', '成本'] + :param optimization_type: 'min'(最小化,如成本)或 'max'(最大化,如消化率) + :return: 字典,含结果字段和值,如 {'构树叶_发酵': {'比例': 14.29, '成本': 85.74}, '总价格': 1989.12} + """ + sense = pulp.LpMinimize if optimization_type == 'min' else pulp.LpMaximize + model = pulp.LpProblem("Feed_Optimization", sense) + + # 决策变量:每种原料比例(0-1,单位:比例) + x = pulp.LpVariable.dicts("比例", ingredients, lowBound=0, upBound=1) + + # 目标函数:最小化总价格(纯价格,无额外权重) + model += pulp.lpSum([x[i] * ingredients[i]['价格'] for i in ingredients]), "总价格" + + # 约束1:总比例=100% + model += pulp.lpSum([x[i] for i in ingredients]) == 1, "总比例" + + # 约束2:营养需求(下限/上限) + for req, value in requirements.items(): + if '下限' in req: + nutrient = req.replace('_下限', '') + # 营养总和≥下限(如蛋白≥17%) + model += pulp.lpSum([x[i] * ingredients[i][nutrient] for i in ingredients]) >= value, req + elif '上限' in req: + nutrient = req.replace('_上限', '') + # 营养总和≤上限(如纤维≤6%) + model += pulp.lpSum([x[i] * ingredients[i][nutrient] for i in ingredients]) <= value, req + + # 求解:用CBC极限精度,深度优先,限时10秒 + try: + solver = pulp.PULP_CBC_CMD(msg=True, + options=['primalT', '1e-12', 'dualT', '1e-12', 'maxIt', '10000000', 'presolve', 'on', + 'strategy', '2', 'randomC', '123', 'sec', '10']) + model.solve(solver) + except AttributeError: + print("警告:CBC不可用,使用默认求解器,精度可能稍低") + model.solve() + + # 检查是否找到最优解 + if pulp.LpStatus[model.status] != 'Optimal': + print("约束值(调试用):") + for name, constraint in model.constraints.items(): + print(f"{name}: {constraint.value()}") + return {"错误": "无解!检查数据(纤维/单宁超?蛋白/氨基酸不足?)"} + + # 构建结果 + result = {} + total_price = pulp.value(model.objective) + result['总价格'] = round(total_price, 2) + result['一斤饲料价格'] = round(total_price / 2000, 2) # 吨=2000斤 + + # 每种原料的比例(%)和成本(元/吨) + for i in ingredients: + result[i] = {field: round(pulp.value(x[i]) * 100, 2) if field == '比例' else round( + pulp.value(x[i]) * ingredients[i]['价格'], 2) for field in result_fields} + + # 计算营养总量 + nutrients = list(set(key for ing in ingredients.values() for key in ing if key not in ['价格'])) + result['营养总量'] = {} + for nut in nutrients: + total = sum(pulp.value(x[i]) * ingredients[i].get(nut, 0) for i in ingredients) + result['营养总量'][nut] = round(total, 2) + + # 检查超标(宁可无解也不超) + for req, value in requirements.items(): + if '上限' in req: + nut = req.replace('_上限', '') + if result['营养总量'][nut] > value + 0.01: # 容忍小误差 + return {"错误": f"{nut}超上限: {result['营养总量'][nut]} > {value} (可能浮点误差)"} + + # 存CSV到results/ + os.makedirs('results', exist_ok=True) + df_formula = pd.DataFrame( + [(k, v['比例'], v['成本']) for k, v in result.items() if k not in ['总价格', '一斤饲料价格', '营养总量']], + columns=['原料', '比例', '成本']) + df_formula.to_csv('results/优化配方结果.csv', index=False) + + # 营养偏差表 + nutrient_table = [] + for nut, value in result['营养总量'].items(): + req_key = f"{nut}_下限" if f"{nut}_下限" in requirements else f"{nut}_上限" + req_type = '下限' if f"{nut}_下限" in requirements else '上限' + req_value = requirements.get(req_key, None) + deviation = value - req_value if req_key.endswith('_下限') else req_value - value + deviation_pct = (deviation / req_value * 100) if req_value != 0 else 0 + status = '达标' if (req_key.endswith('_下限') and value >= req_value) or ( + req_key.endswith('_上限') and value <= req_value) else '不达标' + nutrient_table.append([nut, value, req_value, req_type, deviation, deviation_pct, status]) + df_nutrients = pd.DataFrame(nutrient_table, + columns=['营养', '实际值', '需求值', '需求类型', '偏差', '偏差比例(%)', '状态']) + df_nutrients.to_csv('results/营养总量与需求偏差.csv', index=False) + + return result + + +if __name__ == "__main__": + # 原料数据(单位:%或MJ/kg,价格元/吨,2025年9月农业农村部/饲料市场信息网) + ingredients = { + '构树叶_未加工': {'蛋白': 13, '能量': 8.5, '纤维': 25, '赖氨酸': 0.4, '蛋氨酸': 0.08, '钙': 1.8, '磷': 0.15, + '单宁': 2.5, '价格': 500}, + '构树叶_发酵': {'蛋白': 15, '能量': 9, '纤维': 15, '赖氨酸': 0.5, '蛋氨酸': 0.1, '钙': 1.5, '磷': 0.2, + '单宁': 1.5, '价格': 600}, + '玉米胚芽_压榨': {'蛋白': 11, '能量': 13, '纤维': 7, '赖氨酸': 0.4, '蛋氨酸': 0.2, '钙': 0.1, '磷': 0.3, + '单宁': 0, '价格': 1700}, + '麦麸': {'蛋白': 15, '能量': 10, '纤维': 12, '赖氨酸': 0.4, '蛋氨酸': 0.2, '钙': 0.1, '磷': 1.0, '单宁': 0, + '价格': 1300}, + '米糠': {'蛋白': 13, '能量': 11, '纤维': 10, '赖氨酸': 0.4, '蛋氨酸': 0.2, '钙': 0.1, '磷': 1.5, '单宁': 0, + '价格': 1100}, + '牧草_苜蓿干': {'蛋白': 17, '能量': 8.5, '纤维': 20, '赖氨酸': 0.6, '蛋氨酸': 0.2, '钙': 1.2, '磷': 0.3, + '单宁': 0.5, '价格': 800}, + '玉米': {'蛋白': 9, '能量': 13.5, '纤维': 2.5, '赖氨酸': 0.3, '蛋氨酸': 0.17, '钙': 0.02, '磷': 0.3, '单宁': 0, + '价格': 2367}, + '豆粕': {'蛋白': 46, '能量': 10.5, '纤维': 4, '赖氨酸': 2.8, '蛋氨酸': 0.6, '钙': 0.3, '磷': 0.7, '单宁': 0, + '价格': 3300}, + '植物油_豆油': {'蛋白': 0, '能量': 36, '纤维': 0, '赖氨酸': 0, '蛋氨酸': 0, '钙': 0, '磷': 0, '单宁': 0, + '价格': 7500}, + '赖氨酸_L盐': {'蛋白': 78, '能量': 0, '纤维': 0, '赖氨酸': 78, '蛋氨酸': 0, '钙': 0, '磷': 0, '单宁': 0, + '价格': 15000}, + '蛋氨酸_DL': {'蛋白': 99, '能量': 0, '纤维': 0, '赖氨酸': 0, '蛋氨酸': 99, '钙': 0, '磷': 0, '单宁': 0, + '价格': 25000}, + '预混料_维生素微量元素': {'蛋白': 0, '能量': 0, '纤维': 0, '赖氨酸': 0, '蛋氨酸': 0, '钙': 10, '磷': 5, + '单宁': 0, '价格': 3000}, + '鱼粉': {'蛋白': 60, '能量': 12, '纤维': 1, '赖氨酸': 3.5, '蛋氨酸': 1.5, '钙': 5, '磷': 3, '单宁': 0, + '价格': 5000}, + '骨粉': {'蛋白': 0, '能量': 0, '纤维': 0, '赖氨酸': 0, '蛋氨酸': 0, '钙': 30, '磷': 15, '单宁': 0, + '价格': 1500}, + '食盐': {'蛋白': 0, '能量': 0, '纤维': 0, '赖氨酸': 0, '蛋氨酸': 0, '钙': 0, '磷': 0, '单宁': 0, + '价格': 500} + } + + # 育肥猪需求(30-115kg,NY/T 65-2004+丹麦标准,单位:%或MJ/kg) requirements = { '蛋白_下限': 17, - '能量_下限': 13, - '纤维_上限': 5, - '赖氨酸_下限': 0.75, + '能量_下限': 12.5, + '纤维_上限': 6, + '赖氨酸_下限': 0.8, '蛋氨酸_下限': 0.25, '钙_下限': 0.5, - '磷_下限': 0.4 + '磷_下限': 0.4, + '单宁_上限': 0.5 } - # 运行优化 - result = optimize_feed.optimize_feed(requirements, ingredients, ['比例', '成本'], 'min') - print("优化结果:") - for key, value in result.items(): - print(f"{key}: {value}") + # 优化配方 + result = optimize_feed(requirements, ingredients, ['比例', '成本'], 'min') + + # 美化输出:优化配方 + print("\n=== 优化配方结果 ===") + table_data = [(k, v['比例'], v['成本']) for k, v in result.items() if + k not in ['总价格', '一斤饲料价格', '营养总量']] + headers = ['原料', '比例 (%)', '成本 (元/吨)'] + print(tabulate(table_data, headers=headers, tablefmt='grid', floatfmt='.2f')) + print(f"总价格: {result['总价格']:.2f} 元/吨") + print(f"一斤饲料价格: {result['一斤饲料价格']:.2f} 元") + + # 营养总量与需求偏差 + print("\n=== 营养总量与需求偏差 ===") + nutrient_table = [] + for nut, value in result['营养总量'].items(): + req_key = f"{nut}_下限" if f"{nut}_下限" in requirements else f"{nut}_上限" + req_type = '下限' if f"{nut}_下限" in requirements else '上限' + req_value = requirements.get(req_key, None) + deviation = value - req_value if req_key.endswith('_下限') else req_value - value + deviation_pct = (deviation / req_value * 100) if req_value != 0 else 0 + status = '达标' if (req_key.endswith('_下限') and value >= req_value) or ( + req_key.endswith('_上限') and value <= req_value) else '不达标' + nutrient_table.append([nut, value, req_value, req_type, deviation, deviation_pct, status]) + print(tabulate(nutrient_table, headers=['营养', '实际值', '需求值', '需求类型', '偏差', '偏差比例(%)', '状态'], + tablefmt='grid', floatfmt='.2f')) + + # 市面配方(2025年养猪网,21-35kg调整至30-115kg) + market_formula = { + '玉米': 0.59, + '麦麸': 0.13, + '豆粕': 0.15, + '鱼粉': 0.06, + '骨粉': 0.015, + '食盐': 0.005 + } + + # 市面配方营养和成本 + market_nutrients = calculate_nutrients_from_formula(market_formula, ingredients) + market_cost = calculate_cost_from_formula(market_formula, ingredients) + + print("\n=== 市面配方营养总量 ===") + print(tabulate([(k, v) for k, v in market_nutrients.items()], headers=['营养', '值'], tablefmt='grid', + floatfmt='.2f')) + pd.DataFrame([(k, v) for k, v in market_nutrients.items()], columns=['营养', '值']).to_csv( + 'results/市面配方营养总量.csv', index=False) + + print("\n=== 市面配方成本与偏差 ===") + cost_table = [ + ['总价格 (元/吨)', market_cost['总价格'], result['总价格'], market_cost['总价格'] - result['总价格']], + ['一斤饲料价格 (元)', market_cost['一斤饲料价格'], result['一斤饲料价格'], + market_cost['一斤饲料价格'] - result['一斤饲料价格']] + ] + print(tabulate(cost_table, headers=['项', '市面配方', '优化配方', '偏差'], tablefmt='grid', floatfmt='.2f')) + pd.DataFrame(cost_table, columns=['项', '市面配方', '优化配方', '偏差']).to_csv('results/市面配方成本与偏差.csv', + index=False) + + print("\n所有结果已存至 results/ 文件夹") diff --git a/optimize_feed.py b/optimize_feed.py deleted file mode 100644 index e9c123d..0000000 --- a/optimize_feed.py +++ /dev/null @@ -1,48 +0,0 @@ -import pulp -def optimize_feed(requirements, ingredients, result_fields, optimization_type='min'): - """ - 优化饲料配方,计算最低/最高目标值的原料比例 - :param requirements: 猪营养需求字典,如 {'蛋白_下限': 16, '能量_下限': 12, '纤维_上限': 8} - :param ingredients: 原料数据字典,如 {'构树叶': {'蛋白': 18, '能量': 10, '纤维': 5, '价格': 500}} - :param result_fields: 结果字段列表,如 ['比例', '成本'] - :param optimization_type: 'min'(最小化,如成本)或 'max'(最大化,如消化率) - :return: 字典,含结果字段和值,如 {'构树叶': {'比例': 0.2, '成本': 100}, '总成本': 1475} - """ - # 创建模型 - sense = pulp.LpMinimize if optimization_type == 'min' else pulp.LpMaximize - model = pulp.LpProblem("Feed_Optimization", sense) - - # 决策变量:原料比例(0-1) - x = pulp.LpVariable.dicts("比例", ingredients, lowBound=0, upBound=1) - - # 目标函数:优化指定字段(如价格) - target_field = next(f for f in ingredients[list(ingredients.keys())[0]] if '价格' in f or '成本' in f) # 默认优化价格 - model += pulp.lpSum([x[i] * ingredients[i][target_field] for i in ingredients]), "目标" - - # 约束1:总比例=100% - model += pulp.lpSum([x[i] for i in ingredients]) == 1, "总比例" - - # 约束2:营养需求(下限/上限) - for req, value in requirements.items(): - if '下限' in req: - nutrient = req.replace('_下限', '') - model += pulp.lpSum([x[i] * ingredients[i][nutrient] for i in ingredients]) >= value, req - elif '上限' in req: - nutrient = req.replace('_上限', '') - model += pulp.lpSum([x[i] * ingredients[i][nutrient] for i in ingredients]) <= value, req - - # 求解 - model.solve() - - # 构建结果 - if pulp.LpStatus[model.status] != 'Optimal': - return {"错误": "无解!检查原料数据或需求(比如纤维全超上限?)"} - - result = {} - total_target = pulp.value(model.objective) - result['总' + target_field] = round(total_target, 2) - for i in ingredients: - result[i] = {field: round(pulp.value(x[i]) * 100, 2) if field == '比例' else round( - pulp.value(x[i]) * ingredients[i][target_field], 2) for field in result_fields} - return result - diff --git a/results/优化配方结果.csv b/results/优化配方结果.csv new file mode 100644 index 0000000..c475a6e --- /dev/null +++ b/results/优化配方结果.csv @@ -0,0 +1,16 @@ +原料,比例,成本 +构树叶_未加工,0.0,0.0 +构树叶_发酵,0.0,0.0 +玉米胚芽_压榨,73.58,1250.87 +麦麸,0.0,0.0 +米糠,0.0,0.0 +牧草_苜蓿干,0.0,0.0 +玉米,8.1,191.8 +豆粕,15.79,521.05 +植物油_豆油,0.0,0.0 +赖氨酸_L盐,0.0,0.0 +蛋氨酸_DL,0.0,0.0 +预混料_维生素微量元素,0.0,0.0 +鱼粉,1.52,76.15 +骨粉,1.0,15.06 +食盐,0.0,0.0 diff --git a/results/市面配方成本与偏差.csv b/results/市面配方成本与偏差.csv new file mode 100644 index 0000000..7b06f86 --- /dev/null +++ b/results/市面配方成本与偏差.csv @@ -0,0 +1,3 @@ +项,市面配方,优化配方,偏差 +总价格 (元/吨),2385.53,2054.93,330.60000000000036 +一斤饲料价格 (元),1.19,1.03,0.15999999999999992 diff --git a/results/市面配方营养总量.csv b/results/市面配方营养总量.csv new file mode 100644 index 0000000..44d8dfc --- /dev/null +++ b/results/市面配方营养总量.csv @@ -0,0 +1,9 @@ +营养,值 +蛋氨酸,0.31 +能量,11.56 +赖氨酸,0.86 +钙,0.82 +磷,0.82 +蛋白,17.76 +纤维,3.7 +单宁,0.0 diff --git a/results/营养总量与需求偏差.csv b/results/营养总量与需求偏差.csv new file mode 100644 index 0000000..8984f27 --- /dev/null +++ b/results/营养总量与需求偏差.csv @@ -0,0 +1,9 @@ +营养,实际值,需求值,需求类型,偏差,偏差比例(%),状态 +蛋氨酸,0.28,0.25,下限,0.030000000000000027,12.00000000000001,达标 +能量,12.5,12.5,下限,0.0,0.0,达标 +赖氨酸,0.81,0.8,下限,0.010000000000000009,1.250000000000001,达标 +钙,0.5,0.5,下限,0.0,0.0,达标 +磷,0.55,0.4,下限,0.15000000000000002,37.50000000000001,达标 +蛋白,17.0,17.0,下限,0.0,0.0,达标 +纤维,6.0,6.0,上限,0.0,0.0,达标 +单宁,0.0,0.5,上限,0.5,100.0,达标