diff --git a/README.md b/README.md index a567c2dd4a..160ee2af18 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ -[![Flake8](https://img.shields.io/badge/Flake8-passed-brightgreen)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) -[![Pytest](https://img.shields.io/badge/Pytest-passed-brightgreen)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) -[![Coverage](https://img.shields.io/badge/Coverage-96%25-brightgreen)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) -[![Mypy](https://img.shields.io/badge/Mypy-3210%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) ---- +[![Flake8](https://img.shields.io/badge/Flake8-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Pytest](https://img.shields.io/badge/Pytest-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Coverage](https://img.shields.io/badge/Coverage-%25-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-3216%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Coverage](https://img.shields.io/badge/Coverage-%25-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-3216%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems diff --git a/RUFAS/biophysical/animal/animal.py b/RUFAS/biophysical/animal/animal.py index c5ed690677..ed0d8a42ca 100644 --- a/RUFAS/biophysical/animal/animal.py +++ b/RUFAS/biophysical/animal/animal.py @@ -109,6 +109,8 @@ class Animal: The reproduction submodule that handles the daily reproduction update of the animal. nutrition_requirements: NutrientsRequirements The nutrition requirement for the animal. + calf_nutrition_requirements: dict[str, float] + The nutrition requirement for the calf. nutrition_supply: NutritionSupply The supplied nutrition in the current ration interval for the animal. previous_nutrition_supply: NutritionSupply @@ -193,6 +195,7 @@ def __init__( self.nutrients: Nutrients = Nutrients() self._reproduction: Reproduction = Reproduction() self.nutrition_requirements: NutritionRequirements = NutritionRequirements.make_empty_nutrition_requirements() + self.calf_nutrition_requirements: dict[str, float] = {} self.nutrition_supply: NutritionSupply = NutritionSupply.make_empty_nutrition_supply() self.nutrition_supply.dry_matter = AnimalModuleConstants.DEFAULT_DRY_MATTER_INTAKE self.previous_nutrition_supply: NutritionSupply | None = None @@ -2248,6 +2251,9 @@ def calculate_nutrition_requirements( calf_requirements = CalfRationManager.calc_requirements( self.days_born, self.body_weight, previous_temperature, calf_intake ) + self.calf_nutrition_requirements["whole_milk_intake"] = calf_intake["whole_milk_intake"] + self.calf_nutrition_requirements["milk_replacer_intake"] = calf_intake["milk_replacer_intake"] + self.calf_nutrition_requirements["starter_intake"] = calf_intake["starter_intake"] # TODO: do not use dummy values for calf calcium and phosphorus requirements - issue 2517. return NutritionRequirements( maintenance_energy=calf_requirements["ne_maint"], diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 4c1e2c7f57..d7a61177ad 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -1349,7 +1349,9 @@ def _reformulate_ration_single_pen( Day of simulation. """ - if self.is_ration_defined_by_user is True or pen.animal_combination == AnimalCombination.CALF: + if pen.animal_combination == AnimalCombination.CALF: + pen.handle_calf_ration(self.is_ration_defined_by_user, pen_available_feeds, current_temperature) + elif self.is_ration_defined_by_user is True: pen.use_user_defined_ration(pen_available_feeds, current_temperature) else: if pen.animal_combination == AnimalCombination.LAC_COW and pen.average_milk_production == 0.0: diff --git a/RUFAS/biophysical/animal/pen.py b/RUFAS/biophysical/animal/pen.py index a8d411a1ef..cd9ba64a91 100644 --- a/RUFAS/biophysical/animal/pen.py +++ b/RUFAS/biophysical/animal/pen.py @@ -26,6 +26,7 @@ from RUFAS.biophysical.animal.nutrients.nutrition_evaluator import NutritionEvaluator from RUFAS.biophysical.animal.nutrients.nutrition_supply_calculator import NutritionSupplyCalculator from RUFAS.biophysical.animal.ration.user_defined_ration_manager import UserDefinedRationManager +from RUFAS.biophysical.animal.ration.calf_ration_manager import CalfRationManager from RUFAS.data_structures.feed_storage_to_animal_connection import RUFAS_ID, Feed from RUFAS.biophysical.animal.data_types.animal_combination import AnimalCombination from RUFAS.general_constants import GeneralConstants @@ -1052,7 +1053,7 @@ def use_user_defined_ration(self, pen_available_feeds: list[Feed], temperature: self.reset_milk_production_reduction() self.set_animal_nutritional_requirements(temperature=temperature, available_feeds=pen_available_feeds) ration = UserDefinedRationManager.get_user_defined_ration( - animal_combination, self.average_nutrition_requirements + animal_combination, self.average_nutrition_requirements.dry_matter ) self.set_animal_nutritional_supply(feeds_used=pen_available_feeds, ration_formulation=ration) @@ -1072,7 +1073,7 @@ def use_user_defined_ration(self, pen_available_feeds: list[Feed], temperature: break self.set_animal_nutritional_requirements(temperature=temperature, available_feeds=pen_available_feeds) ration = UserDefinedRationManager.get_user_defined_ration( - animal_combination, self.average_nutrition_requirements + animal_combination, self.average_nutrition_requirements.dry_matter ) self.set_animal_nutritional_supply(feeds_used=pen_available_feeds, ration_formulation=ration) is_ration_adequate, evaluation_result = NutritionEvaluator.evaluate_nutrition_supply( @@ -1086,6 +1087,63 @@ def use_user_defined_ration(self, pen_available_feeds: list[Feed], temperature: self.ration = ration + def handle_calf_ration( + self, is_ration_defined_by_user: bool, pen_available_feeds: list[Feed], temperature: float + ) -> None: + """ + Calculate new ration for the pen based on the number of animals in the pen. + + Parameters + ---------- + is_ration_defined_by_user : bool + True if user defined ration behavior is desired. + pen_available_feeds : list[Feed] + List of available feeds to be used in the ration formulation. + temperature : float + Temperature of the animals' environment (°C). + + Notes + ----- + """ + self.set_animal_nutritional_requirements(temperature=temperature, available_feeds=pen_available_feeds) + total_pen_calf_intake = {"whole_milk_intake": 0.0, "milk_replacer_intake": 0.0, "starter_intake": 0.0} + # wean_day = AnimalBase.config["wean_day"] + # wean_length = AnimalBase.config["wean_length"] + # if 202 in pen_available_feeds["feed_id"]: + # milk_type = "whole" + # else: + # milk_type = "replacer" + for calf in list(self.animals_in_pen.values()): + total_pen_calf_intake["whole_milk_intake"] += calf.calf_nutrition_requirements["whole_milk_intake"] + total_pen_calf_intake["milk_replacer_intake"] + calf.calf_nutrition_requirements["milk_replacer_intake"] + total_pen_calf_intake["starter_intake"] += calf.calf_nutrition_requirements["starter_intake"] + # TODO consider reporting the below in AnimalReporter + # TODO if reported: report for individual, or avg? + # calf_requirements = CalfRationManager.calc_requirements( + # pen.animals_in_pen[calf_id], feed, temp=15, animal_intake=animal_intake + # ) + # TODO Check if we should average the intake, or formulated individual rations + total_calves = len(list(self.animals_in_pen.values())) + average_calf_intake = {intake: value / total_calves for intake, value in total_pen_calf_intake.items()} + ration_per_calf = CalfRationManager.formulate_ration( + [x.rufas_id for x in pen_available_feeds], average_calf_intake + ) + if is_ration_defined_by_user: + dry_matter_intake_requirement = 0.0 + for key in ration_per_calf: + dry_matter_intake_requirement += ration_per_calf[key] + ration_per_calf = UserDefinedRationManager.get_user_defined_ration( + animal_combination=self.animal_combination, dry_matter_intake_requirement=dry_matter_intake_requirement + ) + # TODO deprecate method below if not used here + # ration_per_animal = CalfRationManager.get_average_calf_ration(individual_calf_rations) + # udrm = udr.UserDefinedRationManager() + # if udrm.use_user_defined_ration: + # ration_per_animal = CalfRationManager.make_ration_from_user_values(ration_per_animal) + # ration_vals = {"ME_total": animal_intake["me_intake"]} + self.ration = ration_per_calf + self.set_animal_nutritional_supply(feeds_used=pen_available_feeds, ration_formulation=ration_per_calf) + def get_requested_feed(self, ration_interval_length: int) -> RequestedFeed: """ Returns the requested feed for the pen. diff --git a/RUFAS/biophysical/animal/ration/calf_ration_manager.py b/RUFAS/biophysical/animal/ration/calf_ration_manager.py index f3db74765e..5c06cf561c 100644 --- a/RUFAS/biophysical/animal/ration/calf_ration_manager.py +++ b/RUFAS/biophysical/animal/ration/calf_ration_manager.py @@ -266,3 +266,95 @@ def calc_intake( } return animal_intake + + @classmethod + def formulate_ration(cls, calf_feed_ids: list[int], animal_intake: dict[str, float]) -> dict[int, float]: + """ + Generates formulated ration dictionary per calf. + + Parameters + ---------- + calf_feed_ids : List[int] + List of feed ids available to calves. + animal_intake : Dict[str, int] + Information calculated on a per animal basis for intake required. + + Returns + ------- + Dict[str, float] + Formulated ration. + """ + # TODO update the NASEM library to code in the milk/starter/etc options, use those and RuFaS IDs instead + milk_options = [203, 204, 205, 206, 207] + starter_options = [213, 214, 215, 216, 217, 218] + ration_per_animal = {} + + replacers_selected = [id for id in calf_feed_ids if id in milk_options] + starters_selected = [id for id in calf_feed_ids if id in starter_options] + + for feed_id in calf_feed_ids: + if feed_id == 202: + ration_per_animal[feed_id] = animal_intake["whole_milk_intake"] + elif feed_id in replacers_selected: + ration_per_animal[feed_id] = animal_intake["milk_replacer_intake"] / len(replacers_selected) + elif feed_id in starter_options: + ration_per_animal[feed_id] = animal_intake["starter_intake"] / len(starters_selected) + return ration_per_animal + + @classmethod + def get_average_calf_ration(cls, individual_calf_rations: list[dict[str, float]]) -> dict[str, float | str]: + """ + Get average calf ration for feedout. + + Parameters + ---------- + individual_calf_rations : List[Dict[str, float]] + Each calf's ration. + + Returns + ------- + ration_per_animal : Dict[str, float | str] + Average ration per animal for given calf pen. + """ + ration_per_animal: dict[str, float | str] = {} + for key in individual_calf_rations[0]: + ration_per_animal[key] = 0.0 + for calf_ration in individual_calf_rations: + for key in individual_calf_rations[0]: + ration_per_animal[key] += calf_ration[key] + for key in individual_calf_rations[0]: + ration_per_animal[key] = ration_per_animal[key] / len(individual_calf_rations) + ration_per_animal["status"] = "Optimal" + ration_per_animal["objective"] = 4.5 + return ration_per_animal + + @classmethod + def make_ration_from_user_values(cls, average_calf_ration: dict[str, float]) -> dict[str, float]: + """ + Generate ration dict from user ration percents input, + scaled to their estimated dry matter intake (DMI) + + Parameters + ---------- + average_calf_ration : Dict[str, float] + Formulated ration on a per animal basis using automated methods, for reference to dm total. + + Returns + ------- + Dict[str, float] + dictionary of formulated ration + + """ + ration_per_animal: dict[str, float | str] = {} + total_dm = 0.0 + for key in average_calf_ration: + if key not in ["status", "objective"]: + total_dm += average_calf_ration[key] + # TODO get this working + udrm = udr.UserDefinedRationManager() + for key in udrm.calf_ration: + ration_per_animal[key] = udrm.calf_ration[key] / 100 * total_dm + # TODO check if this needs to be added back + # ration_per_animal["status"] = "Optimal" + # ration_per_animal["objective"] = 0.0 + return ration_per_animal diff --git a/RUFAS/biophysical/animal/ration/user_defined_ration_manager.py b/RUFAS/biophysical/animal/ration/user_defined_ration_manager.py index 39d7a9879e..0f5cb321dc 100644 --- a/RUFAS/biophysical/animal/ration/user_defined_ration_manager.py +++ b/RUFAS/biophysical/animal/ration/user_defined_ration_manager.py @@ -82,7 +82,7 @@ def set_user_defined_rations( def get_user_defined_ration( cls, animal_combination: AnimalCombination, - requirements: NutritionRequirements, + dry_matter_intake_requirement: float, ) -> dict[RUFAS_ID, float]: """ Generate a ration for the given animal type scaled to the estimated dry matter intake requirement. @@ -91,8 +91,8 @@ def get_user_defined_ration( ---------- animal_combination : AnimalCombination The combination of animals in the pen. - requirements : NutritionRequirements - The nutrition requirements of an animal or average of a group of animals. + dry_matter_intake_requirement : float + The estimated dry matter intake requirement. Returns ------- @@ -103,11 +103,7 @@ def get_user_defined_ration( ration_formulation = cls.user_defined_rations[animal_combination] ration: dict[RUFAS_ID, float] = { - rufas_id: ( - requirements.dry_matter * percentage * GeneralConstants.PERCENTAGE_TO_FRACTION - if animal_combination != AnimalCombination.CALF - else cls.CALF_DRY_MATTER_INTAKE * percentage * GeneralConstants.PERCENTAGE_TO_FRACTION - ) + rufas_id: (dry_matter_intake_requirement * percentage * GeneralConstants.PERCENTAGE_TO_FRACTION) for rufas_id, percentage in ration_formulation.items() } diff --git a/RUFAS/routines/animal/animal_manager.py b/RUFAS/routines/animal/animal_manager.py index 09b87c7c36..c43fe896d5 100644 --- a/RUFAS/routines/animal/animal_manager.py +++ b/RUFAS/routines/animal/animal_manager.py @@ -1118,8 +1118,34 @@ def _handle_pen_ration(self, feed: Feed, pen: Pen) -> None: while "status" not in ration_per_animal or ration_per_animal["status"].lower() != "optimal": if pen.animal_combination == AnimalCombination.CALF: - ration_per_animal = CalfRationManager.optimize() - ration_vals = {"ME_total": 0} + individual_calf_rations = [] + wean_day = AnimalBase.config["wean_day"] + wean_length = AnimalBase.config["wean_length"] + if 202 in pen_specific_feed_data["feed_id"]: + milk_type = "whole" + else: + milk_type = "replacer" + for calf_id in pen.animals_in_pen: + animal_intake = CalfRationManager.calc_intake( + pen.animals_in_pen[calf_id], + feed, + wean_day=wean_day, + wean_length=wean_length, + milk_type=milk_type, + ) + # TODO consider reporting the below in AnimalReporter + # calf_requirements = CalfRationManager.calc_requirements( + # pen.animals_in_pen[calf_id], feed, temp=15, animal_intake=animal_intake + # ) + ration_per_calf = CalfRationManager.formulate_ration( + pen_specific_feed_data["feed_id"], animal_intake + ) + individual_calf_rations.append(ration_per_calf) + ration_per_animal = CalfRationManager.get_average_calf_ration(individual_calf_rations) + udrm = udr.UserDefinedRationManager() + if udrm.use_user_defined_ration: + ration_per_animal = CalfRationManager.make_ration_from_user_values(ration_per_animal) + ration_vals = {"ME_total": animal_intake["me_intake"]} else: ration_per_animal, ration_vals = RationManager.formulate_ration( pen, pen_specific_feed_data, self.ANIMAL_GROUPING_SCENARIO, self.simulation_day diff --git a/RUFAS/routines/animal/ration/calf_ration.py b/RUFAS/routines/animal/ration/calf_ration.py index 5c66bc8866..37f457c554 100644 --- a/RUFAS/routines/animal/ration/calf_ration.py +++ b/RUFAS/routines/animal/ration/calf_ration.py @@ -1,6 +1,9 @@ # from .hardcoded_ration import get_ration import math -from typing import Any, Dict +from typing import Any, Dict, List +from RUFAS.routines.feed.feed import Feed +from RUFAS.routines.animal.ration import user_defined_ration as udr +from RUFAS.general_constants import GeneralConstants class CalfRationManager: @@ -43,7 +46,7 @@ def optimize(cls) -> Dict[str, float | str]: def calc_requirements( cls, calf, - feed: Dict[str, float], + feed: Feed, temp: float, animal_intake: Dict[str, int | float], ) -> Dict[str, Dict[str, Any]]: @@ -186,7 +189,7 @@ def calc_requirements( def calc_intake( cls, calf, - feed: Dict[str, float], + feed: Feed, wean_day: int, wean_length: int, milk_type: str, @@ -220,13 +223,13 @@ def calc_intake( starter_id = 216 calf_feeds = feed.calf_feeds - whole_milk_dm = calf_feeds[whole_milk_id]["DM"] - whole_milk_cp = calf_feeds[whole_milk_id]["CP"] de_key = "DE_Base" if "DE_Base" in calf_feeds[whole_milk_id].keys() else "DE" whole_milk_de = calf_feeds[whole_milk_id][de_key] milk_replacer_de = calf_feeds[milk_replacer_id][de_key] starter_de = calf_feeds[starter_id][de_key] + whole_milk_dm = calf_feeds[whole_milk_id]["DM"] + whole_milk_cp = calf_feeds[whole_milk_id]["CP"] # [A.1B.C.1] whole_milk_me = 0.96 * whole_milk_de @@ -247,13 +250,15 @@ def calc_intake( # milk-based feed intake # [A.1B.A.1] - whole_milk_intake = 0.1 * calf.birth_weight * whole_milk_dm * 0.01 + whole_milk_intake = 0.1 * calf.birth_weight * whole_milk_dm * GeneralConstants.PERCENTAGE_TO_FRACTION # [A.1B.A.2] - milk_replacer_intake = 0.1 * calf.birth_weight * 0.15 * milk_replacer_dm * 0.01 + milk_replacer_intake = 0.1 * calf.birth_weight * milk_replacer_dm * GeneralConstants.PERCENTAGE_TO_FRACTION # starter intake # [A.1B.A.3] - if calf.body_weight <= 69.365: + if calf.body_weight <= 50: + starter_intake = 0.01 + elif 50 < calf.body_weight <= 69.365: starter_intake = -0.24783 + 0.0049567 * calf.body_weight else: starter_intake = -6.2263 + 0.091145 * calf.body_weight @@ -314,3 +319,92 @@ def calc_intake( } return animal_intake + + @classmethod + def formulate_ration(cls, calf_feed_ids: List[int], animal_intake: Dict[str, float]) -> Dict[str, float]: + """ + Generates formulated ration dictionary per calf. + + Parameters + ---------- + calf_feed_ids : List[int] + List of feed ids available to calves. + animal_intake : Dict[str, int] + Information calculated on a per animal basis for intake required. + + Returns + ------- + Dict[str, float] + Formulated ration. + """ + milk_options = [203, 204, 205, 206, 207] + starter_options = [213, 214, 215, 216, 217, 218] + ration_per_animal = {} + + replacers_selected = [id for id in calf_feed_ids if id in milk_options] + starters_selected = [id for id in calf_feed_ids if id in starter_options] + + for feed_id in calf_feed_ids: + if feed_id == 202: + ration_per_animal[str(feed_id)] = animal_intake["whole_milk_intake"] + elif feed_id in replacers_selected: + ration_per_animal[str(feed_id)] = animal_intake["milk_replacer_intake"] / len(replacers_selected) + elif feed_id in starter_options: + ration_per_animal[str(feed_id)] = animal_intake["starter_intake"] / len(starters_selected) + return ration_per_animal + + @classmethod + def get_average_calf_ration(cls, individual_calf_rations: List[Dict[str, float]]) -> Dict[str, float | str]: + """ + Get average calf ration for feedout. + + Parameters + ---------- + individual_calf_rations : List[Dict[str, float]] + Each calf's ration. + + Returns + ------- + ration_per_animal : Dict[str, float | str] + Average ration per animal for given calf pen. + """ + ration_per_animal: Dict[str, float | str] = {} + for key in individual_calf_rations[0]: + ration_per_animal[key] = 0.0 + for calf_ration in individual_calf_rations: + for key in individual_calf_rations[0]: + ration_per_animal[key] += calf_ration[key] + for key in individual_calf_rations[0]: + ration_per_animal[key] = ration_per_animal[key] / len(individual_calf_rations) + ration_per_animal["status"] = "Optimal" + ration_per_animal["objective"] = 4.5 + return ration_per_animal + + @classmethod + def make_ration_from_user_values(cls, average_calf_ration: Dict[str, float]) -> Dict[str, float | str]: + """ + Generate ration dict from user ration percents input, + scaled to their estimated dry matter intake (DMI) + + Parameters + ---------- + average_calf_ration : Dict[str, float] + Formulated ration on a per animal basis using automated methods, for reference to dm total. + + Returns + ------- + Dict[str, float] + dictionary of formulated ration + + """ + ration_per_animal: Dict[str, float | str] = {} + total_dm = 0.0 + for key in average_calf_ration: + if key not in ["status", "objective"]: + total_dm += average_calf_ration[key] + udrm = udr.UserDefinedRationManager() + for key in udrm.calf_ration: + ration_per_animal[key] = udrm.calf_ration[key] / 100 * total_dm + ration_per_animal["status"] = "Optimal" + ration_per_animal["objective"] = 0.0 + return ration_per_animal