Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b6dcdb0
base logic for non-user-defined implementation
JoeWaddell Oct 23, 2024
50f72d7
logic for average ration
JoeWaddell Oct 23, 2024
bdca2d7
fixed my wonky logic
JoeWaddell Oct 23, 2024
e43f7f7
added initial udr logic for calves
JoeWaddell Oct 23, 2024
f6b1c16
Merge bdca2d7e8ca4c04312411447f86f15039cf42869 into c5502ffc9a837b8a5…
JoeWaddell Oct 23, 2024
e0d6b34
Apply Black Formatting
github-actions[bot] Oct 23, 2024
857ba16
scaling fix
JoeWaddell Oct 23, 2024
8356be1
Update badges on README
JoeWaddell Oct 23, 2024
1e72e0f
Merge branches 'calf_ration_implement' and 'calf_ration_implement' of…
JoeWaddell Oct 23, 2024
793320e
Merge 1e72e0f23750566d765f7e9b0177f8b12a76e4ba into c5502ffc9a837b8a5…
JoeWaddell Oct 23, 2024
bbb6376
Apply Black Formatting
github-actions[bot] Oct 23, 2024
6e00516
fixing errors
JoeWaddell Oct 23, 2024
717c8ff
Merge branch 'calf_ration_implement' of https://github.com/RuminantFa…
JoeWaddell Oct 23, 2024
0cd4890
Merge 717c8ff4acffff19d7feb71a0dbf2a8522a98fcf into c5502ffc9a837b8a5…
JoeWaddell Oct 23, 2024
c26b0e1
Apply Black Formatting
github-actions[bot] Oct 23, 2024
3331d03
Update badges on README
JoeWaddell Oct 23, 2024
7051294
Update calf_ration.py
tomhuhh Nov 11, 2024
e246839
updating calf replacer and starter intakes
KFosterReed Nov 12, 2024
872ae9f
updating calf replacer and starter intakes
KFosterReed Nov 12, 2024
7de14a6
Merge remote-tracking branch 'origin/calf_ration_implement' into calf…
KFosterReed Nov 12, 2024
2761db1
Merge remote-tracking branch 'origin/calf_ration_implement' into calf…
KFosterReed Nov 12, 2024
bdbd8f7
Merge remote-tracking branch 'origin/calf_ration_implement' into calf…
KFosterReed Nov 12, 2024
d66211b
Fix typo in calf UDR method
KFosterReed Nov 13, 2024
eed9fed
Merge branch 'dev' into calf_ration_implement
JoeWaddell Jul 14, 2025
15a331a
Merge eed9fed9ba71955c131b56a3a615d50424ddb7c8 into 12be8a0296138a0c2…
JoeWaddell Jul 14, 2025
498d610
Apply Black Formatting
github-actions[bot] Jul 14, 2025
ca218d3
Update badges on README
JoeWaddell Jul 14, 2025
1136a43
added calf nutrient requirements to animal
JoeWaddell Jul 16, 2025
cf2f5c1
added handle_calf_ration to pen
JoeWaddell Jul 16, 2025
3e9dcc3
added methods to calf ration manager
JoeWaddell Jul 16, 2025
7edc11f
Merge branch 'calf_ration_implement' of https://github.com/RuminantFa…
JoeWaddell Jul 16, 2025
46d03f0
added udr logic to handle calf ration
JoeWaddell Jul 18, 2025
a5f4da1
updated get_user_defined_ration to only use dry matter intake instead…
JoeWaddell Jul 18, 2025
6472529
updated make_ration_from_user_values for calves
JoeWaddell Jul 18, 2025
aeb4fc7
Merge branch 'dev' into calf_ration_implement
JoeWaddell Jul 31, 2025
5c8c92e
Merge aeb4fc731e4eeb4bfef0f14be9a2411d3aecde67 into d31a0ca0260fb9fbd…
JoeWaddell Jul 31, 2025
789e488
Apply Black Formatting
github-actions[bot] Jul 31, 2025
2c6ea1b
Update badges on README
JoeWaddell Jul 31, 2025
378575a
added elif
JoeWaddell Jul 31, 2025
a8fa3b1
Merge 378575a9588bf0b9b1c12df090f2371304bf40c1 into d31a0ca0260fb9fbd…
JoeWaddell Jul 31, 2025
b0fb53e
Apply Black Formatting
github-actions[bot] Jul 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
6 changes: 6 additions & 0 deletions RUFAS/biophysical/animal/animal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"],
Expand Down
4 changes: 3 additions & 1 deletion RUFAS/biophysical/animal/herd_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
62 changes: 60 additions & 2 deletions RUFAS/biophysical/animal/pen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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(
Expand All @@ -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.
Expand Down
92 changes: 92 additions & 0 deletions RUFAS/biophysical/animal/ration/calf_ration_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 4 additions & 8 deletions RUFAS/biophysical/animal/ration/user_defined_ration_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
-------
Expand All @@ -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()
}

Expand Down
30 changes: 28 additions & 2 deletions RUFAS/routines/animal/animal_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this If-else statement eliminate the need for the user input for "milk_type"? If so, can we delete it while keeping the model backwards compatible for input files that have this input? Or, maybe we just make an issue about user input adjustments that need to be made?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct! The intent here was that it could streamline the feed inputs a little bit...with the user simply selecting the feed IDs for calves as they do for the other animal classes, since it's currently a little redundant.

We could still keep this input, but we'd have to think through the logic of when (or if) it should ever override the user defined feeds.

If we do deprecate it: to maintain backwards compatibility (especially with FARM ES) what I thought is that we could keep the option in the metadata, and simply not let it get to the codebase itself. I think we could/should capture it and flag a warning that the input has been deprecated, and the user should specify the feeds in the feed input JSON (we may want to think about a process for any input field deprecation we do in the future).

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
Expand Down
Loading