217 lines
7.1 KiB
Python
217 lines
7.1 KiB
Python
"""Native derive формул zootech «База сырья» — WESP GfE 2001 engine."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from app.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.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))
|