"""Сверка методик норм: 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)