Интегрирован wesp в сайт
CI / quality (push) Canceled after 0s

This commit is contained in:
влад
2026-07-17 12:57:18 +03:00
parent 5dfa06ddbe
commit 355c0ef9f1
883 changed files with 194576 additions and 177 deletions
+135
View File
@@ -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