@@ -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)
|
||||
Reference in New Issue
Block a user