Интегрирован 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
@@ -0,0 +1 @@
"""Российские методики суточных норм (Москва / Петербург)."""
@@ -0,0 +1,25 @@
"""Линейная интерполяция (аналог FRAC в методичке)."""
from __future__ import annotations
def frac(numerator: float, denominator: float) -> float:
if denominator == 0:
return 0.0
return numerator / denominator
def lerp(x: float, x1: float, n1: float, x2: float, n2: float) -> float:
"""Norma = n1 + frac(n2 - n1, x2 - x1) * (x - x1)."""
if x2 == x1:
return n1
return n1 + frac(n2 - n1, x2 - x1) * (x - x1)
def popr_index(udoy: float, boundaries: list[float]) -> int:
"""1-based индекс столбца POPR_K по суточному удою."""
idx = 1
for i, bound in enumerate(boundaries, start=1):
if udoy >= bound:
idx = i + 1
return min(idx, len(boundaries) + 1)
@@ -0,0 +1,107 @@
"""Методика Москва — лактирующие коровы (NORM_1_1_CALC)."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from app.modules.zootech.lab.calc.racion.interp import popr_index
from app.modules.zootech.lab.calc.norms_derived import apply_derived_norms
from app.modules.zootech.lab.calc.racion.npitv_map import DAIRY_LACTIR_NPITV, NPITV_TO_INDICATOR
from app.modules.zootech.lab.calc.racion.tables import load_moskwa_lactir
@dataclass(frozen=True)
class MoscowDairyParams:
mass_kg: float
milk_yield_kg: float
milk_fat_pct: float = 4.0
lactation_no: int = 2
body_condition: int = 1
housing_system: int = 1
def _row_lookup(rows: list[dict], npitv: int) -> dict | None:
for row in rows:
if row["npitv"] == npitv and row["pom"] == 1:
return row
return None
def _popr_value(row: dict, udoy: float, boundaries: list[float]) -> tuple[float | None, float | None]:
popr = row.get("popr_k") or []
if not popr:
return None, None
idx = popr_index(udoy, boundaries) - 1
idx = max(0, min(idx, len(popr) - 1))
p_k = popr[idx]
koef = float(row.get("koef") or 0)
return p_k, koef
def compute_moscow_norm(npitv: int, params: MoscowDairyParams) -> float | None:
data = load_moskwa_lactir()
boundaries = data.get("udoy_boundaries") or []
row = _row_lookup(data.get("rows") or [], npitv)
if row is None:
return None
mass = params.mass_kg
udoy = params.milk_yield_kg
jir = params.milk_fat_pct
wmassa = mass * 1.02 if params.body_condition > 1 else mass
p_k, koef = _popr_value(row, udoy, boundaries)
if p_k is None:
return None
norma: float | None = None
if npitv == 1:
temp = 0.005 if udoy <= 22 else 0.0025
norma = temp * (wmassa - 500) + p_k * udoy - ((4 - jir) * udoy) / 148
elif npitv == 2:
temp = 0.09 if udoy <= 22 else 0.065
norma = temp * (wmassa - 500) + p_k * udoy - ((4 - jir) * udoy) / 15
elif npitv == 3:
temp = 0.017 if udoy <= 22 else 0.015
norma = temp * (wmassa - 500) + p_k * udoy
elif 4 <= npitv <= 24:
norma = p_k * udoy + (wmassa - 500) * koef
if npitv == 10:
norma *= 0.393
else:
return None
if norma is None:
return None
if params.lactation_no == 1:
norma *= 0.95
elif params.lactation_no == 3:
norma *= 1.05
if params.housing_system == 2:
norma *= 1.1
return round(norma, 3)
def resolve_moscow_dairy_norms(params: MoscowDairyParams) -> tuple[dict[str, dict[str, float | None]], dict[str, Any]]:
resolved: dict[str, dict[str, float | None]] = {}
dynamic: dict[str, Any] = {}
for npitv in DAIRY_LACTIR_NPITV:
value = compute_moscow_norm(npitv, params)
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_moscow", "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_moscow", "source": "derived"}
meta = {
"method": "racion_moscow",
"massKg": params.mass_kg,
"milkYieldKg": params.milk_yield_kg,
"milkFatPct": params.milk_fat_pct,
}
return resolved, {"meta": meta, "dynamic": dynamic}
@@ -0,0 +1,42 @@
"""NPitV (справочник питательных веществ) → ключи показателей WESP."""
from __future__ import annotations
# NORMY_MOSKWA_LACTIR / NORM_1_1_CALC: NPitV 124 (pom=1)
NPITV_TO_INDICATOR: dict[int, str] = {
1: "feed_units",
2: "oe",
3: "dry_matter",
4: "crude_protein",
5: "digestible_protein",
6: "crude_fat",
7: "nel",
8: "rnb",
9: "usp",
10: "sodium",
11: "magnesium",
12: "starch",
13: "potassium",
14: "calcium",
15: "phosphorus",
16: "iron",
17: "copper",
18: "zinc",
19: "manganese",
20: "cobalt",
21: "iodine",
22: "carotene",
23: "vitamin_d",
24: "vitamin_e",
}
DAIRY_LACTIR_NPITV: tuple[int, ...] = tuple(range(1, 25))
# Обратная совместимость
DAIRY_CORE_NPITV: tuple[int, ...] = (2, 3, 4, 5, 7, 8, 9, 10, 12, 14, 15)
INDICATOR_TO_NPITV: dict[str, int] = {v: k for k, v in NPITV_TO_INDICATOR.items()}
def indicator_for_npitv(npitv: int) -> str | None:
return NPITV_TO_INDICATOR.get(npitv)
@@ -0,0 +1,193 @@
"""Методика Петербург — лактирующие коровы (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"]
@@ -0,0 +1,202 @@
"""Подготовка параметров NORM_1_2_PREP (интервалы массы и концентрации)."""
from __future__ import annotations
from dataclasses import dataclass
from app.modules.zootech.lab.calc.racion.interp import lerp
from app.modules.zootech.lab.calc.racion.tables import load_normy_info
@dataclass(frozen=True)
class PiterPrepResult:
mass_ind: int
konc_pred: float
konc_sled: float
m_a: float
m_b: float
udoy_jir: float
wmassa: float
class PiterPrepError(ValueError):
def __init__(self, code: int, message: str, *, info: str | None = None) -> None:
super().__init__(message)
self.code = code
self.info = info
def _info_values(nperem: int) -> list[float]:
data = load_normy_info()
for row in data.get("rows") or []:
if row.get("nperem") == nperem:
return list(row.get("znachenie") or [])
return []
def _fl_from_str(values: list[float], index: int) -> float | None:
"""1-based index как FlFromStr в RACION."""
i = index - 1
if i < 0 or i >= len(values):
return None
return float(values[i])
def _wmassa(mass_kg: float, body_condition: int) -> float:
if body_condition > 1:
return mass_kg * 1.02
return mass_kg
def prepare_piter_calc(
*,
mass_kg: float,
milk_yield_kg: float,
milk_fat_pct: float,
konc_oe_sv: float,
body_condition: int = 1,
) -> PiterPrepResult:
"""Порт NORM_1_2_PREP: интервалы для NORM_1_2_CALC."""
if milk_yield_kg <= 0:
raise PiterPrepError(-11, "Укажите суточный удой, кг")
if milk_fat_pct <= 0:
raise PiterPrepError(-12, "Укажите жирность молока, %")
if mass_kg <= 0:
raise PiterPrepError(-14, "Укажите живую массу, кг")
if konc_oe_sv <= 0:
raise PiterPrepError(-15, "Укажите концентрацию ОЭ/СВ, МДж/кг СВ")
wmassa = _wmassa(mass_kg, body_condition)
udoy_jir = milk_yield_kg * milk_fat_pct * 0.25
kol_konc_vals = _info_values(7)
kol_mass_vals = _info_values(7)
if len(kol_konc_vals) < 2 or len(kol_mass_vals) < 2:
raise PiterPrepError(-1, "Справочник NORMY_INFO (NPerem=7) не задан")
kol_konc = int(kol_konc_vals[0])
kol_mass = int(kol_mass_vals[1])
if kol_konc < 2 or kol_mass < 2:
raise PiterPrepError(-1, "Некорректные размеры таблицы концентраций/масс")
masses = _info_values(6) or _info_values(14)
koncss = _info_values(3)
if not masses or not koncss or len(masses) < 2 or len(koncss) < 2:
raise PiterPrepError(-1, "Справочник NORMY_INFO: массы или концентрации не заданы")
# Интервал концентрации
konc_ind = 1
for i in range(2, kol_konc):
v = _fl_from_str(koncss, i)
if v is not None and konc_oe_sv >= v:
konc_ind = i
konc_pred = _fl_from_str(koncss, konc_ind)
konc_sled = _fl_from_str(koncss, konc_ind + 1)
if konc_pred is None or konc_sled is None or konc_pred < 0 or konc_sled < 0 or konc_pred == konc_sled:
raise PiterPrepError(-1, "Не удалось определить интервал концентрации")
# Интервал массы
mass_ind = 1
for i in range(2, kol_mass):
v = _fl_from_str(masses, i)
if v is not None and wmassa >= v:
mass_ind = i
m_a = _fl_from_str(masses, mass_ind)
m_b = _fl_from_str(masses, mass_ind + 1)
if m_a is None or m_b is None or m_a < 0 or m_b < 0 or m_a == m_b:
raise PiterPrepError(-1, "Не удалось определить интервал массы")
# Проверка удоя (NPerem=4,5)
udoy_str_4 = _info_values(4)
udoy_str_5 = _info_values(5)
udoy_min = _fl_from_str(udoy_str_4, 1) if udoy_str_4 else None
udoy_max = _fl_from_str(udoy_str_5, kol_konc) if udoy_str_5 else None
if udoy_min is not None and udoy_max is not None:
if udoy_jir < udoy_min or udoy_jir > udoy_max:
jir_str = _info_values(2)
gr_udoy1 = max(udoy_min * 4 / milk_fat_pct, udoy_min)
gr_udoy2 = min(udoy_max * 4 / milk_fat_pct, udoy_max)
gr_jir1 = udoy_min * 4 / milk_yield_kg
gr_jir2 = udoy_max * 4 / milk_yield_kg
if jir_str:
if len(jir_str) >= 1:
gr_jir1 = max(gr_jir1, jir_str[0])
if len(jir_str) >= 2:
gr_jir2 = min(gr_jir2, jir_str[1])
code = -4 if udoy_jir < udoy_min else -5
info = f"{gr_udoy1:.4f};{gr_udoy2:.4f};{gr_jir1:.3f};{gr_jir2:.3f};"
raise PiterPrepError(code, "Удой вне допустимого диапазона для жирности", info=info)
# Допустимый диапазон концентрации для udoy_jir
i = 1
if udoy_jir >= (_fl_from_str(udoy_str_4, 1) or 0):
while i < kol_konc - 1:
nxt = _fl_from_str(udoy_str_4, i + 1)
if nxt is None or nxt > udoy_jir:
break
i += 1
else:
while i < kol_konc - 1:
nxt = _fl_from_str(udoy_str_4, i + 1)
cur = _fl_from_str(udoy_str_4, i)
if nxt is None or cur is None or nxt != cur:
break
i += 1
ud_a = _fl_from_str(udoy_str_4, i)
ud_b = _fl_from_str(udoy_str_4, i + 1)
konc_a = _fl_from_str(koncss, i)
konc_b = _fl_from_str(koncss, i + 1)
if ud_a is not None and ud_b is not None and konc_a is not None and konc_b is not None:
if ud_a == ud_b:
konc_max = konc_b
else:
konc_max = lerp(udoy_jir, ud_a, konc_a, ud_b, konc_b)
i = kol_konc
if udoy_jir <= (_fl_from_str(udoy_str_5, kol_konc) or udoy_jir):
while i > 2:
prev = _fl_from_str(udoy_str_5, i - 1)
if prev is None or prev < udoy_jir:
break
i -= 1
else:
while i > 2:
prev = _fl_from_str(udoy_str_5, i - 1)
last = _fl_from_str(udoy_str_5, kol_konc)
if prev is None or last is None or prev != last:
break
i -= 1
ud_a2 = _fl_from_str(udoy_str_5, i - 1)
ud_b2 = _fl_from_str(udoy_str_5, i)
konc_a2 = _fl_from_str(koncss, i - 1)
konc_b2 = _fl_from_str(koncss, i)
if ud_a2 is not None and ud_b2 is not None and konc_a2 is not None and konc_b2 is not None:
if ud_a2 == ud_b2:
konc_min = konc_b2
else:
konc_min = lerp(udoy_jir, ud_a2, konc_a2, ud_b2, konc_b2)
konc_lo = _fl_from_str(koncss, 1) or konc_min
konc_hi = _fl_from_str(koncss, kol_konc) or konc_max
if konc_min < konc_lo:
konc_min = konc_lo
if konc_max > konc_hi:
konc_max = konc_hi
if round(konc_oe_sv, 1) < round(konc_min, 1) or round(konc_oe_sv, 1) > round(konc_max, 1):
code = -2 if round(konc_oe_sv, 1) < round(konc_min, 1) else -3
info = f"{round(konc_min, 1)};{round(konc_max, 1)};"
raise PiterPrepError(
code,
f"Концентрация ОЭ/СВ вне допустимого диапазона ({round(konc_min, 1)}{round(konc_max, 1)})",
info=info,
)
return PiterPrepResult(
mass_ind=mass_ind,
konc_pred=float(konc_pred),
konc_sled=float(konc_sled),
m_a=float(m_a),
m_b=float(m_b),
udoy_jir=udoy_jir,
wmassa=wmassa,
)
@@ -0,0 +1,75 @@
"""Загрузка справочников норм RACION (БД → JSON fallback)."""
from __future__ import annotations
import json
from functools import lru_cache
from pathlib import Path
_SEED_DIR = Path(__file__).resolve().parents[4] / "data" / "seed" / "racion"
def _read_json(name: str) -> dict:
path = _SEED_DIR / name
if not path.exists():
raise FileNotFoundError(f"Справочник не найден: {path}")
return json.loads(path.read_text(encoding="utf-8"))
def _load_from_db(loader_name: str) -> dict | None:
try:
from flask import has_app_context
if not has_app_context():
return None
from app.modules.zootech.lab.services.racion_reference import (
load_moskwa_lactir_from_db,
load_normy_info_from_db,
load_piter_lactir_from_db,
)
loaders = {
"moskwa": load_moskwa_lactir_from_db,
"piter": load_piter_lactir_from_db,
"info": load_normy_info_from_db,
}
fn = loaders.get(loader_name)
if fn is None:
return None
data = fn()
return data if data else None
except Exception:
return None
@lru_cache(maxsize=1)
def load_moskwa_lactir() -> dict:
data = _load_from_db("moskwa")
if data:
return data
return _read_json("moskwa_lactir.json")
@lru_cache(maxsize=1)
def load_piter_lactir() -> dict:
data = _load_from_db("piter")
if data:
return data
return _read_json("piter_lactir.json")
@lru_cache(maxsize=1)
def load_normy_info() -> dict:
data = _load_from_db("info")
if data:
return data
try:
return _read_json("normy_info.json")
except FileNotFoundError:
return {"rows": [], "mass_kg_values": [400, 450, 500, 550, 600, 650, 700, 750]}
def clear_tables_cache() -> None:
load_moskwa_lactir.cache_clear()
load_piter_lactir.cache_clear()
load_normy_info.cache_clear()