136 lines
3.7 KiB
Python
136 lines
3.7 KiB
Python
from __future__ import annotations
|
||
|
||
from typing import Any, Mapping
|
||
|
||
from app.lab.nutrient_schema import resolve_sv_g_per_kg
|
||
|
||
|
||
def normalize_key(value: str) -> str:
|
||
return " ".join((value or "").split()).strip().lower()
|
||
|
||
|
||
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 get_nutrient_value(
|
||
dry_matter: float | None,
|
||
nutrients: Mapping[str, Any] | None,
|
||
nutrient_keys: list[str],
|
||
*,
|
||
indicator_key: str | None = None,
|
||
) -> float | None:
|
||
"""Только точное совпадение ключа (заголовок или indicator_key slug)."""
|
||
nutrients = nutrients or {}
|
||
if indicator_key:
|
||
for k, v in nutrients.items():
|
||
if normalize_key(str(k)) == normalize_key(indicator_key):
|
||
n = parse_num(v)
|
||
if n is not None:
|
||
return n
|
||
for search in nutrient_keys:
|
||
target = normalize_key(search)
|
||
for k, v in nutrients.items():
|
||
if normalize_key(str(k)) != target:
|
||
continue
|
||
n = parse_num(v)
|
||
if n is not None:
|
||
return n
|
||
if any(normalize_key(k) == "св" for k in nutrient_keys):
|
||
return resolve_sv_g_per_kg(nutrients, dry_matter)
|
||
return None
|
||
|
||
|
||
def weighted_average(
|
||
lines: list[dict[str, Any]],
|
||
total_kg: float,
|
||
nutrient_keys: list[str],
|
||
*,
|
||
indicator_key: str | None = None,
|
||
) -> float | None:
|
||
if total_kg <= 0:
|
||
return None
|
||
total = 0.0
|
||
weight = 0.0
|
||
for line in lines:
|
||
kg = float(line.get("daily_kg") or 0)
|
||
if kg <= 0:
|
||
continue
|
||
v = get_nutrient_value(
|
||
line.get("dry_matter"),
|
||
line.get("nutrients"),
|
||
nutrient_keys,
|
||
indicator_key=indicator_key,
|
||
)
|
||
if v is None:
|
||
continue
|
||
total += kg * v
|
||
weight += kg
|
||
if weight <= 0:
|
||
return None
|
||
return total / weight
|
||
|
||
|
||
def daily_intake_total(
|
||
lines: list[dict[str, Any]],
|
||
nutrient_keys: list[str],
|
||
*,
|
||
heads_per_trip: int = 1,
|
||
indicator_key: str | None = None,
|
||
) -> float | None:
|
||
"""Суточная доза на голову (г или МДж): Σ (кг/день/гол × г/кг). Как Excel Рацион КРС."""
|
||
heads = max(int(heads_per_trip or 1), 1)
|
||
total = 0.0
|
||
any_value = False
|
||
for line in lines:
|
||
herd_kg = float(line.get("daily_kg") or 0)
|
||
if herd_kg <= 0:
|
||
continue
|
||
kg = herd_kg / heads
|
||
v = get_nutrient_value(
|
||
line.get("dry_matter"),
|
||
line.get("nutrients"),
|
||
nutrient_keys,
|
||
indicator_key=indicator_key,
|
||
)
|
||
if v is None:
|
||
continue
|
||
total += kg * v
|
||
any_value = True
|
||
return total if any_value else None
|
||
|
||
|
||
def rnb(
|
||
crude_protein: float | None,
|
||
usp: float | None,
|
||
) -> float | None:
|
||
"""RNB (ruminal nitrogen balance, GfE): (сырой протеин − уСП) / 6,25."""
|
||
if crude_protein is None or usp is None:
|
||
return None
|
||
return (crude_protein - usp) / 6.25
|
||
|
||
|
||
def bra_rnb(crude_protein: float | None, usp: float | None) -> float | None:
|
||
"""Deprecated alias for :func:`rnb`."""
|
||
return rnb(crude_protein, usp)
|
||
|
||
|
||
def norm_diff(
|
||
content: float | None,
|
||
min_val: float | None,
|
||
max_val: float | None,
|
||
) -> float | None:
|
||
if content is None:
|
||
return None
|
||
if min_val is not None and content < min_val:
|
||
return content - min_val
|
||
if max_val is not None and content > max_val:
|
||
return content - max_val
|
||
return 0.0
|