Интегрирован 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
+297
View File
@@ -0,0 +1,297 @@
"""Сверка методик норм: WESP, Москва, Петербург."""
from __future__ import annotations
import unittest
from app import create_app, db
from app.lab.calc.norms_derived import apply_derived_norms
from app.lab.calc.norms_resolver import (
NormsParams,
NormsResolveRequest,
merge_hybrid_norms,
resolve_norms,
)
from app.lab.calc.racion.interp import frac, lerp, popr_index
from app.lab.calc.racion.moscow import MoscowDairyParams, compute_moscow_norm, resolve_moscow_dairy_norms
from app.lab.calc.racion.npitv_map import DAIRY_LACTIR_NPITV, NPITV_TO_INDICATOR
from app.lab.calc.racion.piter import PiterDairyParams, compute_piter_norm
from app.lab.calc.racion.piter_prep import PiterPrepResult, prepare_piter_calc
from app.lab.calc.gfe_norms import apply_dynamic_norms
from app.lab.models import LabAnimalProfile, LabProfileNorm
from app.lab.services.profile_norms import sync_racion_norms_to_profile
from app.lab.services.racion_reference import import_racion_reference
from app.services.setup_state import mark_setup_complete
from tests.helpers.zootech_test_helpers import ZootechTestConfig
# Эталон Калашникова 2003, ОЭ МДж (масса 500 кг, жирность 4%)
KALASHNIKOV_OE = {
8: 104,
10: 115,
12: 126,
14: 137,
16: 148,
18: 159,
20: 170,
22: 181,
24: 192,
26: 204,
28: 216,
30: 228,
}
class RacionInterpTests(unittest.TestCase):
def test_frac(self) -> None:
self.assertAlmostEqual(frac(1.0, 4.0), 0.25)
def test_lerp(self) -> None:
self.assertAlmostEqual(lerp(5.0, 0.0, 0.0, 10.0, 10.0), 5.0)
def test_popr_index(self) -> None:
bounds = [8.5, 9.5, 10.5]
self.assertEqual(popr_index(8.0, bounds), 1)
self.assertEqual(popr_index(10.0, bounds), 3)
class NpitvMapTests(unittest.TestCase):
def test_npitv_map_complete(self) -> None:
for npitv in DAIRY_LACTIR_NPITV:
self.assertIn(npitv, NPITV_TO_INDICATOR, f"npitv={npitv}")
self.assertTrue(NPITV_TO_INDICATOR[npitv], f"npitv={npitv}")
class MoscowNormsTests(unittest.TestCase):
def test_oe_kalashnikov_points(self) -> None:
for udoy, expected in KALASHNIKOV_OE.items():
p = MoscowDairyParams(mass_kg=500, milk_yield_kg=float(udoy), milk_fat_pct=4.0)
oe = compute_moscow_norm(2, p)
self.assertIsNotNone(oe, f"udoy={udoy}")
self.assertAlmostEqual(oe, expected, delta=3.0, msg=f"udoy={udoy}")
def test_moscow_all_npitv_smoke(self) -> None:
p = MoscowDairyParams(mass_kg=500, milk_yield_kg=20, milk_fat_pct=4.0)
resolved, _ = resolve_moscow_dairy_norms(p)
for key in ("oe", "nel", "usp", "calcium", "iron", "carotene", "dry_matter"):
self.assertIn(key, resolved, key)
self.assertIsNotNone(resolved[key]["min"], key)
def test_resolver_moscow(self) -> None:
resolved, meta = resolve_norms(
NormsResolveRequest(
method="racion_moscow",
stored={},
mass_kg=500,
milk_yield_kg=20,
params=NormsParams(milk_fat_pct=4.0),
)
)
self.assertEqual(meta["normsMethod"], "racion_moscow")
self.assertIn("oe", resolved)
self.assertAlmostEqual(resolved["oe"]["min"], 170.0, delta=3.0)
self.assertIn("coverage", meta)
self.assertGreater(meta["coverage"]["withMin"], 20)
class PiterNormsTests(unittest.TestCase):
def test_piter_oe_smoke(self) -> None:
p = PiterDairyParams(mass_kg=550, milk_yield_kg=20, milk_fat_pct=4.0, konc_oe_sv=10.3)
oe_low = compute_piter_norm(2, p)
p_high = PiterDairyParams(mass_kg=550, milk_yield_kg=20, milk_fat_pct=4.0, konc_oe_sv=11.0)
oe_high = compute_piter_norm(2, p_high)
self.assertIsNotNone(oe_low)
self.assertIsNotNone(oe_high)
def test_piter_prep_mass_brackets(self) -> None:
prep = prepare_piter_calc(
mass_kg=525,
milk_yield_kg=20,
milk_fat_pct=4.0,
konc_oe_sv=10.3,
)
self.assertIsInstance(prep, PiterPrepResult)
self.assertEqual(prep.m_a, 500.0)
self.assertEqual(prep.m_b, 550.0)
self.assertEqual(prep.mass_ind, 2)
def test_piter_requires_konc(self) -> None:
with self.assertRaises(ValueError):
resolve_norms(
NormsResolveRequest(
method="racion_piter",
stored={},
mass_kg=550,
milk_yield_kg=20,
)
)
class NormsDerivedTests(unittest.TestCase):
def test_norms_derived(self) -> None:
base = {
"dry_matter": {"min": 20000.0, "max": None},
"usp": {"min": 2000.0, "max": None},
}
derived, dyn = apply_derived_norms(base, mass_kg=500)
self.assertIn("usp_per_kg_dm", derived)
self.assertAlmostEqual(derived["usp_per_kg_dm"]["min"], 100.0, places=1)
self.assertIn("usp_per_kg_dm", dyn)
class HybridMergeTests(unittest.TestCase):
def test_hybrid_fallback(self) -> None:
racion = {"oe": {"min": 170.0, "max": None}}
stored = {"metab_lysine": {"min": 50.0, "max": 80.0}}
merged, cov = merge_hybrid_norms(racion, stored)
self.assertAlmostEqual(merged["oe"]["min"], 170.0)
self.assertAlmostEqual(merged["metab_lysine"]["min"], 50.0)
self.assertAlmostEqual(merged["metab_lysine"]["max"], 80.0)
self.assertIn("metab_lysine", cov["fallback"])
class WespResolverRegressionTests(unittest.TestCase):
def test_wesp_matches_apply_dynamic(self) -> None:
stored = {"dry_matter": {"min": 10.0, "max": 20.0}}
legacy, _ = apply_dynamic_norms(stored, mass_kg=650, milk_yield_kg=30, force_dynamic=True)
resolved, meta = resolve_norms(
NormsResolveRequest(
method="wesp",
stored=stored,
mass_kg=650,
milk_yield_kg=30,
force_dynamic=True,
)
)
self.assertEqual(meta["normsMethod"], "wesp")
self.assertAlmostEqual(resolved["usp"]["min"], legacy["usp"]["min"], places=1)
self.assertAlmostEqual(resolved["nel"]["min"], legacy["nel"]["min"], places=1)
class RacionDbImportTests(unittest.TestCase):
def setUp(self) -> None:
self.app = create_app(ZootechTestConfig)
self.ctx = self.app.app_context()
self.ctx.push()
db.create_all()
def tearDown(self) -> None:
db.session.remove()
db.drop_all()
self.ctx.pop()
def test_import_racion_reference(self) -> None:
stats = import_racion_reference(replace=True)
self.assertGreater(stats.moskwa_rows, 20)
self.assertGreater(stats.piter_rows, 100)
self.assertGreater(stats.info_rows, 10)
self.assertFalse(stats.errors)
def test_sync_racion_to_db(self) -> None:
import_racion_reference(replace=True)
profile = LabAnimalProfile(
id="prof-racion-test",
profile_key="dairy_racion_test",
label="Test RACION",
ration_type="DAIRY",
mass_kg=500,
milk_yield_kg=20,
norms_method="racion_moscow",
created_by="test",
)
db.session.add(profile)
db.session.add(
LabProfileNorm(
profile_id=profile.id,
indicator_key="metab_lysine",
min_value=45.0,
max_value=60.0,
)
)
db.session.commit()
n = sync_racion_norms_to_profile(profile)
db.session.commit()
self.assertGreater(n, 20)
norms = LabProfileNorm.query.filter_by(profile_id=profile.id).all()
self.assertGreaterEqual(len(norms), 20)
oe = LabProfileNorm.query.filter_by(profile_id=profile.id, indicator_key="oe").first()
self.assertIsNotNone(oe)
self.assertIsNotNone(oe.min_value)
lys = LabProfileNorm.query.filter_by(profile_id=profile.id, indicator_key="metab_lysine").first()
self.assertIsNotNone(lys)
self.assertEqual(lys.min_value, 45.0)
class NormsPreviewApiTests(unittest.TestCase):
def setUp(self) -> None:
self.app = create_app(ZootechTestConfig)
self.client = self.app.test_client()
self.ctx = self.app.app_context()
self.ctx.push()
db.create_all()
mark_setup_complete(self.app)
self.client.post(
"/api/auth/login",
json={"login": "zootech-test-admin", "password": "zootech-test-secret"},
)
def tearDown(self) -> None:
db.session.remove()
db.drop_all()
self.ctx.pop()
def test_norms_preview_moscow(self) -> None:
r = self.client.get("/api/lab/norms-preview?method=racion_moscow&mass_kg=500&milk_yield_kg=10")
self.assertEqual(r.status_code, 200, r.get_data(as_text=True))
body = r.get_json()
self.assertEqual(body["normsMethod"], "racion_moscow")
self.assertAlmostEqual(body["resolvedIndicators"]["oe"]["min"], 115.0, delta=1.0)
self.assertIn("coverage", body)
self.assertGreater(body["coverage"]["withMin"], 20)
def test_norms_preview_piter_requires_konc(self) -> None:
r = self.client.get("/api/lab/norms-preview?method=racion_piter&mass_kg=550&milk_yield_kg=20")
self.assertEqual(r.status_code, 400)
class ProfileApiCoverageTests(unittest.TestCase):
def setUp(self) -> None:
self.app = create_app(ZootechTestConfig)
self.client = self.app.test_client()
self.ctx = self.app.app_context()
self.ctx.push()
db.create_all()
mark_setup_complete(self.app)
self.client.post(
"/api/auth/login",
json={"login": "zootech-test-admin", "password": "zootech-test-secret"},
)
def tearDown(self) -> None:
db.session.remove()
db.drop_all()
self.ctx.pop()
def test_profile_get_exposes_racion_coverage(self) -> None:
profile = LabAnimalProfile(
profile_key="cov_dairy",
label="Coverage",
ration_type="DAIRY",
mass_kg=500,
milk_yield_kg=20,
norms_method="racion_moscow",
)
db.session.add(profile)
db.session.commit()
db.session.add(
LabProfileNorm(profile_id=profile.id, indicator_key="metab_lysine", min_value=45.0, max_value=60.0)
)
db.session.commit()
r = self.client.get(f"/api/lab/animal-profiles/{profile.id}")
self.assertEqual(r.status_code, 200, r.get_data(as_text=True))
body = r.get_json()
self.assertIn("coverage", body)
self.assertIn("resolvedNorms", body)
self.assertGreater(body["coverage"]["withMin"], 20)
self.assertIn("metab_lysine", body["coverage"]["fallback"])
self.assertIn("oe", body["coverage"]["racion"])
self.assertEqual(body["norms"]["metab_lysine"]["min"], 45.0)