@@ -0,0 +1,135 @@
|
||||
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
|
||||
Reference in New Issue
Block a user