Files
site/apps/api/app/modules/zootech/lab/calc/ingredient_derive.py
T
2026-07-17 12:57:18 +03:00

217 lines
7.1 KiB
Python

"""Native derive формул zootech «База сырья» — WESP GfE 2001 engine."""
from __future__ import annotations
from typing import Any
from app.modules.zootech.lab.calc.gfe_policies import (
DCAB_CL,
DCAB_K,
DCAB_NA,
DCAB_S,
DEFAULT_CP_DIGEST_PCT,
DEFAULT_FAT_DIGEST_PCT,
DEFAULT_FIBER_DIGEST_PCT,
DEFAULT_INSOLUBLE_PROTEIN_PCT,
DEFAULT_NFE_DIGEST_PCT,
DEFAULT_PROTEIN_FRACTION_PCT,
DeriveContext,
GE_CP,
GE_FAT,
GE_FIBER,
GE_NFE,
ME_CP,
ME_FAT,
ME_FIBER,
ME_OR_RESIDUE,
NEL_BASE,
NEL_Q_COEFF,
NEL_Q_REF_PCT,
USP_FAT_THRESHOLD_G_PER_KG_DM,
default_om_digestibility_pct,
)
from app.modules.zootech.lab.calc.ingredient_catalog import (
DISPLAY_SYNC,
HEADER_TO_LETTER,
INGREDIENT_HEADERS,
LETTER_TO_HEADER,
)
def _parse_num(value: Any) -> float | None:
if value is None or value == "":
return None
try:
n = float(value)
except (TypeError, ValueError):
return None
return None if n != n else n
def _normalize_key(value: str) -> str:
return " ".join((value or "").split()).strip().lower()
def _v(cells: dict[str, float], letter: str, default: float = 0.0) -> float:
return cells.get(letter, default)
def _if_pos(test: float, when_true, when_false: float = 0.0) -> float:
"""Excel IF(test>0, …) — ветка when_true не вычисляется при test<=0."""
if test > 0:
return when_true() if callable(when_true) else when_true
return when_false
def dict_to_cells(data: dict[str, Any] | None) -> dict[str, float]:
"""Словарь {заголовок: значение} → {буква колонки: значение}."""
cells: dict[str, float] = {}
if not data:
return cells
norm_index = {_normalize_key(h): h for h in INGREDIENT_HEADERS}
for raw_key, raw_val in data.items():
n = _parse_num(raw_val)
if n is None:
continue
nk = _normalize_key(str(raw_key))
header = norm_index.get(nk)
if header is None:
continue
letter = HEADER_TO_LETTER.get(header)
if letter:
cells[letter] = n
return cells
def cells_to_dict(cells: dict[str, float]) -> dict[str, float]:
out: dict[str, float] = {}
for letter, value in cells.items():
header = LETTER_TO_HEADER.get(letter)
if header and header not in ("№", "Наименование", "Цена 1 кг"):
out[header] = value
return out
def derive_cells(
cells: dict[str, float],
*,
context: DeriveContext | None = None,
) -> dict[str, float]:
"""Пересчёт derived-колонок по цепочке формул row 6 «База сырья»."""
c = dict(cells)
ctx = context or DeriveContext.infer_from_cells(c)
omd_default = default_om_digestibility_pct(ctx)
c["AE"] = DCAB_NA * _v(c, "AA") + DCAB_K * _v(c, "AB") - DCAB_CL * _v(c, "AC") - DCAB_S * _v(c, "AD")
c["AG"] = _v(c, "AJ") * _v(c, "AI") / 100.0
c["AF"] = _v(c, "AI") - c["AG"] + _v(c, "AH")
c["AX"] = _v(c, "D") - _v(c, "F") - _v(c, "K") - _v(c, "M") - _v(c, "AW")
c["BN"] = _if_pos(_v(c, "E"), lambda: _v(c, "D") / _v(c, "E") * 1000.0)
c["BP"] = _if_pos(
_v(c, "BO"),
lambda: (_v(c, "D") - _v(c, "AW")) * _v(c, "BO") / 100.0,
(_v(c, "D") - _v(c, "AW")) * omd_default / 100.0,
)
c["BR"] = _if_pos(
_v(c, "BQ"), lambda: _v(c, "F") * _v(c, "BQ") / 100.0, _v(c, "F") * DEFAULT_CP_DIGEST_PCT / 100.0
)
c["BT"] = _if_pos(
_v(c, "BS"), lambda: _v(c, "M") * _v(c, "BS") / 100.0, _v(c, "M") * DEFAULT_FAT_DIGEST_PCT / 100.0
)
c["BV"] = _if_pos(
_v(c, "BU"), lambda: _v(c, "K") * _v(c, "BU") / 100.0, _v(c, "K") * DEFAULT_FIBER_DIGEST_PCT / 100.0
)
c["BX"] = _if_pos(
_v(c, "BW"), lambda: c["AX"] * _v(c, "BW") / 100.0, c["AX"] * DEFAULT_NFE_DIGEST_PCT / 100.0
)
c["BY"] = GE_CP * _v(c, "F") + GE_FAT * _v(c, "M") + GE_FIBER * _v(c, "K") + GE_NFE * c["AX"]
c["BZ"] = (
ME_FAT * c["BT"]
+ ME_FIBER * c["BV"]
+ ME_OR_RESIDUE * (c["BP"] - c["BT"] - c["BV"])
+ ME_CP * _v(c, "F")
)
c["CA"] = _if_pos(
c["BY"],
lambda: (NEL_BASE * (1.0 + NEL_Q_COEFF * (c["BZ"] / c["BY"] * 100.0 - NEL_Q_REF_PCT)) * c["BZ"]),
)
c["CF"] = _if_pos(
_v(c, "CE"),
lambda: _v(c, "F") * _v(c, "CE") / 100.0,
_v(c, "F") * DEFAULT_INSOLUBLE_PROTEIN_PCT / 100.0,
)
c["CG"] = _if_pos(_v(c, "D"), lambda: _v(c, "M") * 1000.0 / _v(c, "D"))
c["CH"] = _if_pos(_v(c, "D"), lambda: c["CF"] * 1000.0 / _v(c, "D"))
c["CI"] = _if_pos(_v(c, "D"), lambda: _v(c, "F") * 1000.0 / _v(c, "D"))
c["CJ"] = _if_pos(_v(c, "D"), lambda: c["BP"] / _v(c, "D"))
c["CK"] = _if_pos(_v(c, "D"), lambda: c["BT"] / _v(c, "D"))
c["CL"] = _if_pos(c["CI"], lambda: (187.7 - 115.4 * c["CH"] / c["CI"]) * c["CJ"] + 1.03 * c["CH"])
c["CM"] = _if_pos(c["CI"], lambda: (196.1 - 127.5 * c["CH"] / c["CI"]) * (c["CJ"] - c["CK"]) + 1.03 * c["CH"])
co = _v(c, "CO")
if c["CG"] < USP_FAT_THRESHOLD_G_PER_KG_DM + 0.01:
c["CN"] = c["CL"]
elif c["CG"] > USP_FAT_THRESHOLD_G_PER_KG_DM:
c["CN"] = c["CM"]
else:
c["CN"] = 0.0
if co < 1.01:
c["CP"] = c["CN"] * _v(c, "D") / 1000.0
elif co > 1.0:
c["CP"] = co
else:
c["CP"] = 0.0
c["CQ"] = (_v(c, "F") - c["CP"]) / 6.25
c["CX"] = _if_pos(
_v(c, "CT"),
lambda: _v(c, "F") * _v(c, "CT") / 100.0,
_v(c, "F") * DEFAULT_PROTEIN_FRACTION_PCT / 100.0,
)
nel = c["CA"]
c["CY"] = nel * _v(c, "CW")
f_val = _v(c, "F")
bo = _v(c, "BO")
if f_val == 0.0:
c["CZ"] = 0.0
c["DA"] = 0.0
c["DB"] = 0.0
c["DC"] = 0.0
c["DD"] = 0.0
c["DE"] = 0.0
else:
c["DA"] = c["CX"] * (_v(c, "AY") / 10.0) / (f_val / 10.0) * (bo / 100.0) + c["CY"] * 0.071 * 0.8
c["CZ"] = _if_pos(
bo,
lambda: c["CX"] * (_v(c, "AZ") / 10.0) / (f_val / 10.0) * (bo / 100.0) + c["CY"] * 0.018 * 0.8,
)
c["DB"] = _if_pos(
bo,
lambda: c["CX"] * (_v(c, "BA") / 10.0) / (f_val / 10.0) * (bo / 100.0) + c["CY"] * 0.044 * 0.8,
)
c["DC"] = _if_pos(
_v(c, "BD"),
lambda: c["CX"] * (_v(c, "BD") / 10.0) / (f_val / 10.0) * (bo / 100.0) + c["CY"] * 0.063 * 0.8,
)
c["DD"] = _if_pos(
_v(c, "BC"),
lambda: c["CX"] * (_v(c, "BC") / 10.0) / (f_val / 10.0) * (bo / 100.0) + c["CY"] * 0.049 * 0.8,
)
c["DE"] = _if_pos(
_v(c, "BE"),
lambda: c["CX"] * (_v(c, "BE") / 10.0) / (f_val / 10.0) * (bo / 100.0) + c["CY"] * 0.048 * 0.8,
)
for display, source in DISPLAY_SYNC:
c[display] = c[source]
return c
def derive_ingredient_nutrients(
data: dict[str, Any] | None,
*,
context: DeriveContext | None = None,
) -> dict[str, float]:
"""Полный набор показателей: входные + пересчитанные derived."""
cells = dict_to_cells(data)
return cells_to_dict(derive_cells(cells, context=context))