Files
site/apps/api/app/modules/zootech/lab/calc/racion/piter.py
T
2026-07-17 12:57:18 +03:00

194 lines
6.8 KiB
Python

"""Методика Петербург — лактирующие коровы (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"]