"""Методика Петербург — лактирующие коровы (NORM_1_2_CALC).""" from __future__ import annotations from dataclasses import dataclass from typing import Any from app.modules.zootech.lab.calc.norms_derived import apply_derived_norms from app.modules.zootech.lab.calc.racion.interp import lerp from app.modules.zootech.lab.calc.racion.moscow import MoscowDairyParams from app.modules.zootech.lab.calc.racion.npitv_map import DAIRY_LACTIR_NPITV, NPITV_TO_INDICATOR from app.modules.zootech.lab.calc.racion.piter_prep import PiterPrepError, PiterPrepResult, prepare_piter_calc from app.modules.zootech.lab.calc.racion.tables import load_piter_lactir @dataclass(frozen=True) class PiterDairyParams(MoscowDairyParams): konc_oe_sv: float = 10.3 def _konc_bracket(konc: float, konc_values: list[float]) -> tuple[float, float]: sorted_k = sorted(set(konc_values)) positive = [k for k in sorted_k if k > 0] if not positive: return konc, konc if konc <= positive[0]: return positive[0], positive[min(1, len(positive) - 1)] for i in range(len(positive) - 1): if positive[i] <= konc <= positive[i + 1]: return positive[i], positive[i + 1] return positive[-2], positive[-1] def _udoy_bracket(udoy_jir: float, udoys: list[float]) -> tuple[float, float]: sorted_u = sorted(set(udoys)) if len(sorted_u) < 2: return sorted_u[0], sorted_u[0] if udoy_jir <= sorted_u[0]: return sorted_u[0], sorted_u[1] for i in range(len(sorted_u) - 1): if sorted_u[i] <= udoy_jir < sorted_u[i + 1]: return sorted_u[i], sorted_u[i + 1] return sorted_u[-2], sorted_u[-1] def _norm_at_mass( entry_normy: list[float], mass_ind: int, wmassa: float, m_a: float, m_b: float, ) -> float | None: i = mass_ind - 1 if i < 0 or i + 1 >= len(entry_normy): return None n_a, n_b = entry_normy[i], entry_normy[i + 1] if n_a <= 0 or n_b <= 0: return None return lerp(wmassa, m_a, n_a, m_b, n_b) def _entries_for(data: dict, npitv: int) -> list[dict]: npv = 4 if npitv == 5 else npitv return [e for e in data.get("entries") or [] if e["npitv"] == npv] def _compute_with_konc( entries: list[dict], npitv: int, params: PiterDairyParams, prep: PiterPrepResult, konc_pred: float, konc_sled: float, ) -> float | None: by_konc = {k: [e for e in entries if abs(e["konc"] - k) < 1e-6] for k in (konc_pred, konc_sled)} def _at_konc(konc: float) -> float | None: rows = by_konc.get(konc) or [] if not rows: return None udoys = [e["udoy"] for e in rows] ud_pred, ud_sled = _udoy_bracket(prep.udoy_jir, udoys) if ud_pred == ud_sled: return None e_pred = next((e for e in rows if e["udoy"] == ud_pred), None) e_sled = next((e for e in rows if e["udoy"] == ud_sled), None) if not e_pred or not e_sled: return None n01 = _norm_at_mass(e_pred["normy"], prep.mass_ind, prep.wmassa, prep.m_a, prep.m_b) n02 = _norm_at_mass(e_sled["normy"], prep.mass_ind, prep.wmassa, prep.m_a, prep.m_b) if n01 is None or n02 is None: return None return lerp(prep.udoy_jir, ud_pred, n01, ud_sled, n02) norma1 = _at_konc(konc_pred) norma2 = _at_konc(konc_sled) if norma1 is None or norma2 is None or konc_pred == konc_sled: return None norma = lerp(params.konc_oe_sv, konc_pred, norma1, konc_sled, norma2) if npitv == 5: norma *= 0.65 return round(norma, 3) def _compute_konc_independent( entries: list[dict], params: PiterDairyParams, prep: PiterPrepResult, ) -> float | None: rows = [e for e in entries if e["konc"] == -10] if not rows: return None udoys = [e["udoy"] for e in rows] ud_pred, ud_sled = _udoy_bracket(prep.udoy_jir, udoys) if ud_pred == ud_sled: return None e_pred = next((e for e in rows if e["udoy"] == ud_pred), None) e_sled = next((e for e in rows if e["udoy"] == ud_sled), None) if not e_pred or not e_sled: return None n01 = _norm_at_mass(e_pred["normy"], prep.mass_ind, prep.wmassa, prep.m_a, prep.m_b) n02 = _norm_at_mass(e_sled["normy"], prep.mass_ind, prep.wmassa, prep.m_a, prep.m_b) if n01 is None or n02 is None: return None norma = lerp(prep.udoy_jir, ud_pred, n01, ud_sled, n02) if params.housing_system == 2: norma *= 1.1 return round(norma, 3) def compute_piter_norm(npitv: int, params: PiterDairyParams, prep: PiterPrepResult | None = None) -> float | None: data = load_piter_lactir() entries = _entries_for(data, npitv) if not entries: return None if prep is None: prep = prepare_piter_calc( mass_kg=params.mass_kg, milk_yield_kg=params.milk_yield_kg, milk_fat_pct=params.milk_fat_pct, konc_oe_sv=params.konc_oe_sv, body_condition=params.body_condition, ) konc_vals = sorted({e["konc"] for e in entries if e["konc"] > 0}) if konc_vals: min_k = min(konc_vals) if min_k > 0: konc_pred, konc_sled = _konc_bracket(params.konc_oe_sv, konc_vals) return _compute_with_konc(entries, npitv, params, prep, konc_pred, konc_sled) return _compute_konc_independent(entries, params, prep) def resolve_piter_dairy_norms(params: PiterDairyParams) -> tuple[dict[str, dict[str, float | None]], dict[str, Any]]: prep = prepare_piter_calc( mass_kg=params.mass_kg, milk_yield_kg=params.milk_yield_kg, milk_fat_pct=params.milk_fat_pct, konc_oe_sv=params.konc_oe_sv, body_condition=params.body_condition, ) resolved: dict[str, dict[str, float | None]] = {} dynamic: dict[str, Any] = {} for npitv in DAIRY_LACTIR_NPITV: value = compute_piter_norm(npitv, params, prep=prep) if value is None: continue key = NPITV_TO_INDICATOR.get(npitv) if not key: continue resolved[key] = {"min": value, "max": None} dynamic[key] = {"min": value, "npitv": npitv, "method": "racion_piter", "source": "racion"} resolved, derived_dyn = apply_derived_norms(resolved, mass_kg=params.mass_kg) for k, v in derived_dyn.items(): dynamic[k] = {**v, "method": "racion_piter", "source": "derived"} meta = { "method": "racion_piter", "massKg": params.mass_kg, "milkYieldKg": params.milk_yield_kg, "koncOeSv": params.konc_oe_sv, "prep": { "massInd": prep.mass_ind, "mA": prep.m_a, "mB": prep.m_b, "koncPred": prep.konc_pred, "koncSled": prep.konc_sled, }, } return resolved, {"meta": meta, "dynamic": dynamic} __all__ = ["PiterDairyParams", "PiterPrepError", "compute_piter_norm", "resolve_piter_dairy_norms"]