@@ -0,0 +1,229 @@
|
||||
"""Регрессия маппинга нутриентов: NFC, fuzzy-match, derived pct/g per kg, EAV slugs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from app import create_app, db
|
||||
from app.lab.calc.derived import apply_content_derived
|
||||
from app.lab.calc.engine import calculate_ration
|
||||
from app.lab.calc.ingredient_derive import derive_ingredient_nutrients, dict_to_cells
|
||||
from app.lab.calc.nutrients import get_nutrient_value
|
||||
from app.lab.indicators import indicator_by_key
|
||||
from app.lab.nutrient_keys import augment_with_indicator_keys, canonicalize_for_storage
|
||||
from app.lab.nutrient_schema import read_from_mapping
|
||||
from app.lab.services.component_nutrients import (
|
||||
nutrients_calc_dict,
|
||||
nutrients_full_dict,
|
||||
pin_nutrient_values,
|
||||
upsert_from_api_dict,
|
||||
)
|
||||
from app.models.component import Component
|
||||
from app.services.setup_state import mark_setup_complete
|
||||
from tests.helpers.zootech_test_helpers import ZootechTestConfig
|
||||
|
||||
|
||||
class IngredientDeriveKeyExactMatchTests(unittest.TestCase):
|
||||
def test_dict_to_cells_does_not_map_rup_per_kg_to_sv(self) -> None:
|
||||
"""Нер. СП / кг СВ (11) не должен перезаписывать СВ (850) в derive."""
|
||||
cells = dict_to_cells(
|
||||
{
|
||||
"СВ": 850.0,
|
||||
"Нер. СП / кг СВ": 11.0,
|
||||
"% уСП/кг СВ": 7.3,
|
||||
}
|
||||
)
|
||||
self.assertEqual(cells.get("D"), 850.0)
|
||||
|
||||
def test_derive_digestible_nfe_positive_on_seno_seed_shape(self) -> None:
|
||||
derived = derive_ingredient_nutrients(
|
||||
{
|
||||
"СВ": 850,
|
||||
"Сыр. Протеин": 78,
|
||||
"Сырая клетч": 328,
|
||||
"Сырой жир": 22,
|
||||
"Сырая зола": 65,
|
||||
"БЕР": 95,
|
||||
"уСП": 62,
|
||||
"Нер. СП / кг СВ": 11,
|
||||
"% уСП/кг СВ": 7.3,
|
||||
}
|
||||
)
|
||||
self.assertGreater(derived.get("Переварим БЭВ", 0), 0)
|
||||
self.assertGreater(derived.get("Перевар Орг Вещ", 0), 0)
|
||||
self.assertGreater(derived.get("уСП/кг СВ формул", 0), 0)
|
||||
|
||||
|
||||
class NutrientKeyExactMatchTests(unittest.TestCase):
|
||||
def test_read_mapping_ca_not_dcab(self) -> None:
|
||||
data = {"Ca": 3.5, "DCAB Форм": 120.0, "DCAB": 99.0}
|
||||
self.assertEqual(read_from_mapping(data, ("DCAB Форм", "DCAB")), 120.0)
|
||||
self.assertEqual(read_from_mapping(data, ("Ca",)), 3.5)
|
||||
|
||||
def test_fat_pct_key_does_not_read_fat_per_kg_dm_column(self) -> None:
|
||||
"""Старый fuzzy-match подставлял СЖ/кг СВ (г/кг) в %-СЖ/кг СВ (%)."""
|
||||
nutrients = {"СЖ/кг СВ": 2260.0, "Сырой жир": 22.0}
|
||||
v = get_nutrient_value(
|
||||
850,
|
||||
nutrients,
|
||||
["%-СЖ/кг СВ ()"],
|
||||
indicator_key="fat_pct_per_kg_dm",
|
||||
)
|
||||
self.assertIsNone(v)
|
||||
|
||||
def test_nfc_key_not_in_nfc_pct_indicator_keys(self) -> None:
|
||||
defn = indicator_by_key("nfc_pct_dm")
|
||||
self.assertIsNotNone(defn)
|
||||
keys = [k.lower() for k in defn["nutrient_keys"]]
|
||||
self.assertNotIn("nfc", keys)
|
||||
self.assertEqual(defn.get("derived"), "pct_of_dm")
|
||||
self.assertEqual(defn.get("from_key"), "nfe")
|
||||
|
||||
|
||||
class DerivedContentTests(unittest.TestCase):
|
||||
def test_pct_of_dm(self) -> None:
|
||||
defn = {"derived": "pct_of_dm", "from_key": "crude_fat"}
|
||||
content = {"crude_fat": 220.0, "dry_matter": 8500.0}
|
||||
self.assertAlmostEqual(apply_content_derived(content, defn), 2.588235, places=4)
|
||||
|
||||
def test_g_per_kg_dm_daily_totals(self) -> None:
|
||||
defn = {"derived": "g_per_kg_dm", "from_key": "crude_protein"}
|
||||
content = {"crude_protein": 1000.0, "dry_matter": 8500.0}
|
||||
self.assertAlmostEqual(apply_content_derived(content, defn), 117.647, places=2)
|
||||
|
||||
def test_g_per_kg_dm_compound_mode_returns_source(self) -> None:
|
||||
defn = {"derived": "g_per_kg_dm", "from_key": "crude_fat"}
|
||||
content = {"crude_fat": 28.0, "dry_matter": 650.0}
|
||||
self.assertEqual(
|
||||
apply_content_derived(content, defn, compound_mode=True),
|
||||
28.0,
|
||||
)
|
||||
|
||||
def test_nel_per_kg_dm_daily_vs_compound(self) -> None:
|
||||
defn = {"derived": "nel_per_kg_dm"}
|
||||
daily = {"nel": 32.0, "dry_matter": 8500.0}
|
||||
compound = {"nel": 7.8, "dry_matter": 880.0}
|
||||
self.assertAlmostEqual(apply_content_derived(daily, defn), 3.7647, places=3)
|
||||
self.assertAlmostEqual(apply_content_derived(compound, defn, compound_mode=True), 7.8)
|
||||
|
||||
|
||||
class AugmentIndicatorKeysTests(unittest.TestCase):
|
||||
def test_header_duplicated_as_indicator_slug(self) -> None:
|
||||
out = augment_with_indicator_keys({"Сырой жир": 22.0, "БЭВ": 95.0})
|
||||
self.assertEqual(out["Сырой жир"], 22.0)
|
||||
self.assertEqual(out["crude_fat"], 22.0)
|
||||
self.assertEqual(out["nfe"], 95.0)
|
||||
|
||||
def test_canonicalize_preserves_headers_and_slugs(self) -> None:
|
||||
stored = canonicalize_for_storage({"Сыр. Протеин": 170.0})
|
||||
self.assertEqual(stored["Сыр. Протеин"], 170.0)
|
||||
self.assertEqual(stored["crude_protein"], 170.0)
|
||||
|
||||
|
||||
class NutrientMappingRationRegressionTests(unittest.TestCase):
|
||||
_LINE = {
|
||||
"daily_kg": 10,
|
||||
"in_ration": True,
|
||||
"in_compound": False,
|
||||
"dry_matter": 850,
|
||||
"nutrients": {
|
||||
"СВ": 850,
|
||||
"Сырой жир": 22,
|
||||
"БЕР": 95,
|
||||
"NFC": 520,
|
||||
"СЖ/кг СВ": 2260,
|
||||
},
|
||||
}
|
||||
|
||||
def _indicator(self, result: dict, key: str) -> float:
|
||||
return next(i["content"] for i in result["indicators"] if i["key"] == key)
|
||||
|
||||
def test_nfc_g_per_kg_not_used_for_nfc_pct(self) -> None:
|
||||
result = calculate_ration("DAIRY", [self._LINE], {}, heads_per_trip=1)
|
||||
nfc_pct = self._indicator(result, "nfc_pct_dm")
|
||||
self.assertAlmostEqual(nfc_pct, 11.176, places=1)
|
||||
self.assertLess(nfc_pct, 50.0)
|
||||
|
||||
def test_fat_pct_from_crude_fat_daily_total_not_per_kg_column(self) -> None:
|
||||
result = calculate_ration("DAIRY", [self._LINE], {}, heads_per_trip=1)
|
||||
fat_pct = self._indicator(result, "fat_pct_per_kg_dm")
|
||||
self.assertAlmostEqual(fat_pct, 2.588, places=1)
|
||||
self.assertLess(fat_pct, 10.0)
|
||||
|
||||
def test_cp_per_kg_dm_from_daily_total(self) -> None:
|
||||
line = {
|
||||
**self._LINE,
|
||||
"nutrients": {
|
||||
**self._LINE["nutrients"],
|
||||
"Сыр. Протеин": 100,
|
||||
},
|
||||
}
|
||||
result = calculate_ration("DAIRY", [line], {}, heads_per_trip=1)
|
||||
cp = self._indicator(result, "crude_protein")
|
||||
dm = self._indicator(result, "dry_matter")
|
||||
cp_kg = self._indicator(result, "cp_per_kg_dm")
|
||||
self.assertAlmostEqual(cp_kg, cp / (dm / 1000.0), places=2)
|
||||
|
||||
|
||||
class ComponentNutrientsEavTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.app = create_app(ZootechTestConfig)
|
||||
self.ctx = self.app.app_context()
|
||||
self.ctx.push()
|
||||
db.create_all()
|
||||
mark_setup_complete(self.app)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
db.session.remove()
|
||||
db.drop_all()
|
||||
self.ctx.pop()
|
||||
|
||||
def test_upsert_adds_indicator_key_slugs_to_eav(self) -> None:
|
||||
comp = Component(name="LAB regression NFC", type="зерновые", dry_matter=88.0, price=1.0)
|
||||
db.session.add(comp)
|
||||
db.session.commit()
|
||||
|
||||
upsert_from_api_dict(
|
||||
comp.id,
|
||||
{
|
||||
"СВ": 880,
|
||||
"Сыр. Протеин": 170,
|
||||
"Сырая клетч": 95,
|
||||
"Сырой жир": 38,
|
||||
"БЭВ": 520,
|
||||
"NFC": 999,
|
||||
},
|
||||
)
|
||||
full = nutrients_full_dict(comp.id)
|
||||
self.assertEqual(full.get("crude_fat"), 38.0)
|
||||
self.assertIn("crude_protein", full)
|
||||
self.assertEqual(full.get("NFC"), 999.0)
|
||||
|
||||
def test_calc_dict_uses_ber_for_nfe_not_nfc_in_ration(self) -> None:
|
||||
comp = Component(name="LAB regression calc", type="зерновые", dry_matter=88.0, price=1.0)
|
||||
db.session.add(comp)
|
||||
db.session.commit()
|
||||
pin_nutrient_values(
|
||||
comp.id,
|
||||
{"СВ": 880, "БЕР": 95, "NFC": 520, "Сырой жир": 38},
|
||||
)
|
||||
calc = nutrients_calc_dict(comp.id)
|
||||
result = calculate_ration(
|
||||
"DAIRY",
|
||||
[
|
||||
{
|
||||
"daily_kg": 5,
|
||||
"in_ration": True,
|
||||
"in_compound": False,
|
||||
"dry_matter": calc.get("СВ") or 880,
|
||||
"nutrients": calc,
|
||||
}
|
||||
],
|
||||
{},
|
||||
heads_per_trip=1,
|
||||
)
|
||||
nfc_pct = next(i["content"] for i in result["indicators"] if i["key"] == "nfc_pct_dm")
|
||||
nfc_from_wrong_key = 520 / 880 * 100
|
||||
self.assertAlmostEqual(nfc_pct, 95 / 880 * 100, places=1)
|
||||
self.assertNotAlmostEqual(nfc_pct, nfc_from_wrong_key, places=0)
|
||||
self.assertLess(nfc_pct, 50.0)
|
||||
Reference in New Issue
Block a user