From a88ef86f767a07678852d704edf545fb3c1ce84b Mon Sep 17 00:00:00 2001 From: Allister Liu Date: Fri, 27 Feb 2026 09:47:03 -0500 Subject: [PATCH 1/6] 1st draft --- RUFAS/biophysical/animal/animal.py | 56 +++++++++---------- RUFAS/biophysical/animal/animal_config.py | 27 +++++++-- .../animal/data_types/animal_typed_dicts.py | 2 + .../animal/data_types/reproduction.py | 8 +++ RUFAS/biophysical/animal/herd_manager.py | 39 ++++++++++++- .../animal/reproduction/reproduction.py | 28 +++++++++- RUFAS/input_manager.py | 1 - RUFAS/task_manager.py | 1 + .../data/animal/example_freestall_animal.json | 28 ++++++++-- input/metadata/properties/default.json | 37 +++++++----- 10 files changed, 171 insertions(+), 56 deletions(-) diff --git a/RUFAS/biophysical/animal/animal.py b/RUFAS/biophysical/animal/animal.py index 5975e98c27..6667200594 100644 --- a/RUFAS/biophysical/animal/animal.py +++ b/RUFAS/biophysical/animal/animal.py @@ -1137,33 +1137,33 @@ def milk_statistics(self) -> MilkProductionStatistics: ranking_index=self.genetics.ranking_index, ) - def _assign_sex_to_newborn_calf(self) -> None: - """ - Assign a sex to a newborn calf based on the semen type and male calf rate. - - Determines the sex of the calf by evaluating the type of semen used (conventional - or sexed) and the corresponding male calf rate. Raises a ValueError if an - unexpected semen type is encountered. - - Raises - ------ - ValueError - If `AnimalConfig.semen_type` is not "conventional" or "sexed". - - """ - if AnimalConfig.semen_type == "conventional": - male_calf_rate = AnimalConfig.male_calf_rate_conventional_semen - elif AnimalConfig.semen_type == "sexed": - male_calf_rate = AnimalConfig.male_calf_rate_sexed_semen - else: - om = OutputManager() - om.add_error( - "Unexpected semen type", - f"Unexpected semen type: {AnimalConfig.semen_type}", - {"class": self.__class__.__name__, "function": self._assign_sex_to_newborn_calf.__name__}, - ) - raise ValueError(f"Unexpected semen type: {AnimalConfig.semen_type}") - self.sex = Sex.MALE if random() < male_calf_rate else Sex.FEMALE + # def _assign_sex_to_newborn_calf(self) -> None: + # """ + # Assign a sex to a newborn calf based on the semen type and male calf rate. + # + # Determines the sex of the calf by evaluating the type of semen used (conventional + # or sexed) and the corresponding male calf rate. Raises a ValueError if an + # unexpected semen type is encountered. + # + # Raises + # ------ + # ValueError + # If `AnimalConfig.semen_type` is not "conventional" or "sexed". + # + # """ + # if AnimalConfig.semen_type == "conventional": + # male_calf_rate = AnimalConfig.male_calf_rate_conventional_semen + # elif AnimalConfig.semen_type == "sexed": + # male_calf_rate = AnimalConfig.male_calf_rate_sexed_semen + # else: + # om = OutputManager() + # om.add_error( + # "Unexpected semen type", + # f"Unexpected semen type: {AnimalConfig.semen_type}", + # {"class": self.__class__.__name__, "function": self._assign_sex_to_newborn_calf.__name__}, + # ) + # raise ValueError(f"Unexpected semen type: {AnimalConfig.semen_type}") + # self.sex = Sex.MALE if random() < male_calf_rate else Sex.FEMALE def _initialize_newborn_calf(self, args: NewBornCalfValuesTypedDict, simulation_day: int) -> None: """ @@ -1178,7 +1178,7 @@ def _initialize_newborn_calf(self, args: NewBornCalfValuesTypedDict, simulation_ The current day in the simulation, used for event logging and status evaluation. """ - self._assign_sex_to_newborn_calf() + self.sex = args["sex"] if random() < AnimalConfig.still_birth_rate: self.stillborn_day = simulation_day diff --git a/RUFAS/biophysical/animal/animal_config.py b/RUFAS/biophysical/animal/animal_config.py index c4d1a0ed53..7400c11325 100644 --- a/RUFAS/biophysical/animal/animal_config.py +++ b/RUFAS/biophysical/animal/animal_config.py @@ -198,6 +198,28 @@ class AnimalConfig: semen_type: str = "conventional" male_calf_rate_conventional_semen: float = 0.53 male_calf_rate_sexed_semen: float = 0.10 + semen_allocation: list[dict[str, float | str]] = [ + { + "lower_percentage": 0, + "upper_percentage": 25, + "semen_type": "conventional_beef", + }, + { + "lower_percentage": 25, + "upper_percentage": 50, + "semen_type": "sexed_beef", + }, + { + "lower_percentage": 50, + "upper_percentage": 75, + "semen_type": "conventional_dairy", + }, + { + "lower_percentage": 75, + "upper_percentage": 100, + "semen_type": "sexed_dairy", + }, + ] keep_female_calf_rate: float = 1 still_birth_rate: float = 0.065 average_gestation_length: int = 276 @@ -398,10 +420,7 @@ def initialize_animal_config(cls) -> None: cls.cull_milk_production = animal_config_data["management_decisions"]["cull_milk_production"] cls.semen_type = animal_config_data["management_decisions"]["semen_type"] - cls.male_calf_rate_conventional_semen = animal_config_data["farm_level"]["calf"][ - "male_calf_rate_conventional_semen" - ] - cls.male_calf_rate_sexed_semen = animal_config_data["farm_level"]["calf"]["male_calf_rate_sexed_semen"] + cls.semen_allocation = animal_config_data["management_decisions"]["semen_allocation"] cls.keep_female_calf_rate = animal_config_data["farm_level"]["calf"]["keep_female_calf_rate"] cls.still_birth_rate = animal_config_data["from_literature"]["life_cycle"]["still_birth_rate"] cls.average_gestation_length = animal_config_data["farm_level"]["repro"]["avg_gestation_len"] diff --git a/RUFAS/biophysical/animal/data_types/animal_typed_dicts.py b/RUFAS/biophysical/animal/data_types/animal_typed_dicts.py index 7ed608bd5c..488dec011e 100644 --- a/RUFAS/biophysical/animal/data_types/animal_typed_dicts.py +++ b/RUFAS/biophysical/animal/data_types/animal_typed_dicts.py @@ -2,6 +2,7 @@ from typing_extensions import NotRequired +from RUFAS.biophysical.animal.data_types.animal_enums import Sex from RUFAS.biophysical.animal.data_types.body_weight_history import BodyWeightHistory from RUFAS.biophysical.animal.data_types.pen_history import PenHistory @@ -30,6 +31,7 @@ class NewBornCalfValuesTypedDict(TypedDict): id: NotRequired[int] breed: str + sex: Sex animal_type: str birth_date: str days_born: int diff --git a/RUFAS/biophysical/animal/data_types/reproduction.py b/RUFAS/biophysical/animal/data_types/reproduction.py index 78c77f169d..3104d3d807 100644 --- a/RUFAS/biophysical/animal/data_types/reproduction.py +++ b/RUFAS/biophysical/animal/data_types/reproduction.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from enum import Enum from RUFAS.biophysical.animal.data_types.animal_enums import Breed from RUFAS.biophysical.animal.data_types.animal_events import AnimalEvents @@ -329,3 +330,10 @@ def is_pregnant(self) -> bool: @property def is_milking(self) -> bool: return self.days_in_milk > 0 + + +class SemenType(Enum): + SEXED_BEEF = "sexed_beef" + CONVENTIONAL_BEEF = "conventional_beef" + SEXED_DAIRY = "sexed_dairy" + CONVENTIONAL_DAIRY = "conventional_dairy" diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 88c3b82a2e..3595bd3a72 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -3,6 +3,8 @@ import math from typing import Any +import numpy as np + from RUFAS.biophysical.animal import animal_constants from RUFAS.biophysical.animal.animal import Animal from RUFAS.biophysical.animal.animal_config import AnimalConfig @@ -22,7 +24,7 @@ from RUFAS.biophysical.animal.data_types.animal_types import AnimalType from RUFAS.biophysical.animal.data_types.daily_routines_output import DailyRoutinesOutput from RUFAS.biophysical.animal.data_types.milk_production import MilkProductionStatistics -from RUFAS.biophysical.animal.data_types.reproduction import HerdReproductionStatistics +from RUFAS.biophysical.animal.data_types.reproduction import HerdReproductionStatistics, SemenType from RUFAS.biophysical.animal.herd_factory import HerdFactory from RUFAS.biophysical.animal.milk.lactation_curve import LactationCurve from RUFAS.biophysical.animal.milk.milk_production import MilkProduction @@ -165,7 +167,8 @@ def __init__( herd_population.cows, herd_population.replacement, ) - + self._assign_semen_type_for_new_heiferIIs(self.heiferIIs) + self._assign_semen_type_for_cows(self.cows) self.allocate_animals_to_pens(time.simulation_day) self.initialize_nutrient_requirements(weather, time, available_feeds) @@ -517,11 +520,42 @@ def _perform_daily_routines_for_animals( group_specific_TBV_fat_mean=mean_tbv_fat, group_specific_TBV_protein_mean=mean_tbv_protein, ) + self._assign_semen_type_for_cow_giving_birth(animal) elif animal_daily_routines_output.animal_status in [AnimalStatus.DEAD, AnimalStatus.SOLD]: sold_animals.append(animal) animal.update_genetic_history(simulation_day=time.simulation_day) return (graduated_animals, sold_animals, stillborn_newborn_calves, newborn_calves, sold_newborn_calves) + def _assign_semen_type_for_new_heiferIIs(self, new_heiferIIs: list[Animal]) -> None: + heiferII_ranking_indexes: list[float] = [heifer_II.genetics.ranking_index for heifer_II in self.heiferIIs] + for heiferII in new_heiferIIs: + ranking_index = heiferII.genetics.ranking_index + semen_type = self._find_semen_type(heiferII_ranking_indexes, ranking_index) + heiferII.reproduction.semen_type = semen_type + + def _assign_semen_type_for_cows(self, cows: list[Animal]) -> None: + cow_ranking_indexes: list[float] = [cow.genetics.ranking_index for cow in self.cows] + for cow in cows: + ranking_index = cow.genetics.ranking_index + semen_type = self._find_semen_type(cow_ranking_indexes, ranking_index) + cow.reproduction.semen_type = semen_type + + def _assign_semen_type_for_cow_giving_birth(self, cow: Animal) -> None: + cow_ranking_indexes: list[float] = [cow.genetics.ranking_index for cow in self.cows] + semen_type = self._find_semen_type(cow_ranking_indexes, cow.genetics.ranking_index) + cow.reproduction.semen_type = semen_type + + def _find_semen_type(self, population_ranking_index: list[float], animal_ranking_index: float) -> SemenType: + animal_ranking_index_percentile: float = np.mean( + np.array(population_ranking_index) <= animal_ranking_index) * 100 + for allocation in sorted(AnimalConfig.semen_allocation, key=lambda x: x["lower_percentage"]): + lower = allocation["lower_percentage"] + upper = allocation["upper_percentage"] + if lower <= animal_ranking_index_percentile < upper or (upper == 100 and animal_ranking_index_percentile == 100): + return SemenType(allocation["semen_type"]) + self.om.add_error("", "", {}) + raise ValueError() + def _update_herd_structure( self, graduated_animals: list[Animal], @@ -591,6 +625,7 @@ def daily_routines( graduated_heiferIs, sold_heiferIs, _, _, _ = self._perform_daily_routines_for_animals(time, self.heiferIs) graduated_animals += graduated_heiferIs removed_animals += sold_heiferIs + self._assign_semen_type_for_new_heiferIIs(graduated_heiferIs) graduated_heiferIIs, sold_heiferIIs, _, _, _ = self._perform_daily_routines_for_animals(time, self.heiferIIs) graduated_animals += graduated_heiferIIs diff --git a/RUFAS/biophysical/animal/reproduction/reproduction.py b/RUFAS/biophysical/animal/reproduction/reproduction.py index 341e8d813f..b8dcc54d03 100644 --- a/RUFAS/biophysical/animal/reproduction/reproduction.py +++ b/RUFAS/biophysical/animal/reproduction/reproduction.py @@ -7,7 +7,7 @@ from RUFAS.biophysical.animal import animal_constants from RUFAS.biophysical.animal.animal_config import AnimalConfig -from RUFAS.biophysical.animal.data_types.animal_enums import Breed +from RUFAS.biophysical.animal.data_types.animal_enums import Breed, Sex from RUFAS.biophysical.animal.data_types.animal_typed_dicts import NewBornCalfValuesTypedDict from RUFAS.biophysical.animal.data_types.animal_types import AnimalType from RUFAS.biophysical.animal.data_types.preg_check_config import PregnancyCheckConfig @@ -26,7 +26,7 @@ ReproductionInputs, ReproductionDataStream, AnimalReproductionStatistics, - HerdReproductionStatistics, + HerdReproductionStatistics, SemenType, ) from RUFAS.biophysical.animal.reproduction.hormone_delivery_schedule import HormoneDeliverySchedule @@ -120,6 +120,8 @@ def __init__( do_not_breed: bool = False, estrus_count: int = 0, ) -> None: + self.semen_type: SemenType | None = None + self.embryo_sex: Sex | None = None self.heifer_reproduction_program = heifer_reproduction_program or AnimalConfig.heifer_reproduction_program self.heifer_reproduction_sub_program = ( heifer_reproduction_sub_program or AnimalConfig.heifer_reproduction_sub_program @@ -268,6 +270,9 @@ def cow_reproduction_update( Updated reproduction datastream for the cow. """ if reproduction_data_stream.is_pregnant and reproduction_data_stream.days_in_pregnancy == self.gestation_length: + if time.simulation_day == 0: + # TODO: randomize sex for cows giving birth on day 0 + self.embryo_sex = self._determine_embryo_sex(time.simulation_day) reproduction_data_stream = self.cow_give_birth(reproduction_data_stream, time) if not self.do_not_breed: @@ -452,6 +457,7 @@ def cow_give_birth( reproduction_data_stream.newborn_calf_config = NewBornCalfValuesTypedDict( breed=reproduction_data_stream.breed.name, + sex=self.embryo_sex, animal_type=AnimalType.CALF.value, birth_date=time.current_date.strftime("%Y-%m-%d"), days_born=0, @@ -1008,7 +1014,7 @@ def _perform_ai( reproduction_data_stream.events.add_event( reproduction_data_stream.days_born, simulation_day, - animal_constants.INSEMINATED_W_BASE + AnimalConfig.semen_type, + animal_constants.INSEMINATED_W_BASE + self.semen_type.name, ) self.reproduction_statistics.semen_number += 1 self.reproduction_statistics.AI_times += 1 @@ -1030,6 +1036,7 @@ def _perform_ai( reproduction_data_stream, simulation_day ) reproduction_data_stream = self._increment_successful_cow_conceptions(reproduction_data_stream) + self.embryo_sex = self._determine_embryo_sex(simulation_day) else: if reproduction_data_stream.animal_type == AnimalType.HEIFER_II: reproduction_data_stream = self._handle_failed_heifer_conception( @@ -1040,6 +1047,18 @@ def _perform_ai( return reproduction_data_stream + def _determine_embryo_sex(self, simulation_day) -> Sex: + # TODO: put male calf rate into Animal Constant + if self.semen_type in [SemenType.CONVENTIONAL_DAIRY, SemenType.CONVENTIONAL_BEEF]: + male_calf_rate = 0.5 + elif self.semen_type in [SemenType.SEXED_DAIRY, SemenType.SEXED_BEEF]: + male_calf_rate = 0.9 + else: + print(simulation_day, self.semen_type) + raise ValueError("Unexpected Semen Type.") + embryo_sex = Sex.MALE if random.random() < male_calf_rate else Sex.FEMALE + return embryo_sex + def _increment_heifer_ai_counts(self, reproduction_data_stream: ReproductionDataStream) -> ReproductionDataStream: """Increment the AI counts for heifers.""" @@ -1118,6 +1137,8 @@ def _handle_failed_heifer_conception( average_estrus_cycle, std_estrus_cycle, ) + # TODO: set sex to None when animal loses preg + self.embryo_sex = None return reproduction_data_stream def _calculate_gestation_length(self) -> int: @@ -1965,6 +1986,7 @@ def _handle_failed_cow_conception( simulation_day, f"Current repro state(s): {self.repro_state_manager}", ) + self.embryo_sex = None return reproduction_data_stream def cow_pregnancy_update( diff --git a/RUFAS/input_manager.py b/RUFAS/input_manager.py index 440722431e..b8f8c7ab80 100644 --- a/RUFAS/input_manager.py +++ b/RUFAS/input_manager.py @@ -30,7 +30,6 @@ "config", "animal", "animal_population", - "animal_net_merit", "animal_top_listing_semen", "lactation", "economy", diff --git a/RUFAS/task_manager.py b/RUFAS/task_manager.py index d9517d9087..b978428edd 100644 --- a/RUFAS/task_manager.py +++ b/RUFAS/task_manager.py @@ -281,6 +281,7 @@ def check_dependencies(self) -> None: " dependencies at required minimum levels.", {"class": TaskManager.__name__, "function": TaskManager.check_dependencies.__name__}, ) + return raise RuntimeError( f"[ERROR] {package_name}=={installed_version} does not satisfy required version:" f" {requirement.specifier}" diff --git a/input/data/animal/example_freestall_animal.json b/input/data/animal/example_freestall_animal.json index beb4a2f956..a940bd8da8 100644 --- a/input/data/animal/example_freestall_animal.json +++ b/input/data/animal/example_freestall_animal.json @@ -28,18 +28,38 @@ "heifer_repro_method": "SynchED", "cow_repro_method": "TAI", "semen_type": "conventional", + "semen_allocation": [ + { + "lower_percentage": 0, + "upper_percentage": 25, + "semen_type": "conventional_beef" + }, + { + "lower_percentage": 25, + "upper_percentage": 50, + "semen_type": "sexed_beef" + }, + { + "lower_percentage": 50, + "upper_percentage": 75, + "semen_type": "conventional_dairy" + }, + { + "lower_percentage": 75, + "upper_percentage": 100, + "semen_type": "sexed_dairy" + } + ], "days_in_preg_when_dry": 218, "heifer_repro_cull_time": 500, "do_not_breed_time": 185, "cull_milk_production": 30, "cow_times_milked_per_day": 3, - "milk_fat_percent": 4, - "milk_protein_percent": 3.2 + "milk_fat_percent": 4, + "milk_protein_percent": 3.2 }, "farm_level": { "calf": { - "male_calf_rate_sexed_semen": 0.1, - "male_calf_rate_conventional_semen": 0.53, "keep_female_calf_rate": 1, "wean_day": 60, "wean_length": 7, diff --git a/input/metadata/properties/default.json b/input/metadata/properties/default.json index 8b1699ac68..dd930c793a 100644 --- a/input/metadata/properties/default.json +++ b/input/metadata/properties/default.json @@ -184,6 +184,29 @@ "default": "conventional", "pattern": "^(sexed|conventional)$" }, + "semen_allocation": { + "type": "array", + "description": "", + "properties": { + "type": "object", + "lower_percentage": { + "type": "number", + "description": "", + "minimum": 0, + "maximum": 100 + }, + "upper_percentage": { + "type": "number", + "description": "", + "minimum": 0, + "maximum": 100 + }, + "semen_type": { + "type": "string", + "description": "" + } + } + }, "days_in_preg_when_dry": { "type": "number", "description": "Days In Pregnancy When Dry (days) -- The average days in pregnancy of cows at dry-off", @@ -233,20 +256,6 @@ "calf": { "type": "object", "description": "Calf Management", - "male_calf_rate_sexed_semen": { - "type": "number", - "description": "Male Calf Rate, Sexed Semen (percent) -- The rate of male calves when using sexed semen", - "default": 0.1, - "minimum": 0, - "maximum": 1 - }, - "male_calf_rate_conventional_semen": { - "type": "number", - "description": "Male Calf Rate, Conventional Semen (percent) -- The rate of male calves when using conventional semen", - "default": 0.53, - "minimum": 0, - "maximum": 1 - }, "keep_female_calf_rate": { "type": "number", "description": "Female Calf Retention Rate (percent) -- The percentage of female calves kept and raised on-farm", From a01580829c8b8587a9f336d29b4bdcea50eac459 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 17 Apr 2026 02:14:45 +0000 Subject: [PATCH 2/6] Apply Black Formatting --- RUFAS/biophysical/animal/herd_manager.py | 10 +++++++--- RUFAS/biophysical/animal/reproduction/reproduction.py | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 3e192b982e..e2c92526df 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -546,15 +546,19 @@ def _assign_semen_type_for_cow_giving_birth(self, cow: Animal) -> None: cow.reproduction.semen_type = semen_type def _find_semen_type(self, population_ranking_index: list[float], animal_ranking_index: float) -> SemenType: - animal_ranking_index_percentile: float = np.mean( - np.array(population_ranking_index) <= animal_ranking_index) * 100 + animal_ranking_index_percentile: float = ( + np.mean(np.array(population_ranking_index) <= animal_ranking_index) * 100 + ) for allocation in sorted(AnimalConfig.semen_allocation, key=lambda x: x["lower_percentage"]): lower = allocation["lower_percentage"] upper = allocation["upper_percentage"] - if lower <= animal_ranking_index_percentile < upper or (upper == 100 and animal_ranking_index_percentile == 100): + if lower <= animal_ranking_index_percentile < upper or ( + upper == 100 and animal_ranking_index_percentile == 100 + ): return SemenType(allocation["semen_type"]) self.om.add_error("", "", {}) raise ValueError() + def _update_genetic_values_at_lactation_start(self, animal: Animal, time: RufasTime) -> None: """ Updates the genetic values of an animal at the start of a new lactation. diff --git a/RUFAS/biophysical/animal/reproduction/reproduction.py b/RUFAS/biophysical/animal/reproduction/reproduction.py index 167e33e993..94ac84fce6 100644 --- a/RUFAS/biophysical/animal/reproduction/reproduction.py +++ b/RUFAS/biophysical/animal/reproduction/reproduction.py @@ -27,7 +27,8 @@ ReproductionInputs, ReproductionDataStream, AnimalReproductionStatistics, - HerdReproductionStatistics, SemenType, + HerdReproductionStatistics, + SemenType, ) from RUFAS.biophysical.animal.reproduction.hormone_delivery_schedule import HormoneDeliverySchedule From 7121850e633cd20d1ebdbc03e5b0e770f76409f0 Mon Sep 17 00:00:00 2001 From: allisterakun Date: Fri, 17 Apr 2026 02:19:03 +0000 Subject: [PATCH 3/6] Update badges on README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2024cc3be2..79309f000b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![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-99%25-brightgreen)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) -[![Mypy](https://img.shields.io/badge/Mypy-1180%20errors-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-1229%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems From 2520b68d98aac5b9471b46ce7e32a6b17c8ed71a Mon Sep 17 00:00:00 2001 From: Allister Liu Date: Mon, 20 Apr 2026 09:04:54 -0400 Subject: [PATCH 4/6] wip --- RUFAS/biophysical/animal/animal.py | 28 ++++++++- RUFAS/biophysical/animal/animal_config.py | 32 ++++------ .../animal/data_types/reproduction.py | 7 ++- RUFAS/biophysical/animal/herd_manager.py | 58 +++---------------- .../animal/reproduction/reproduction.py | 39 ++++++++++++- 5 files changed, 86 insertions(+), 78 deletions(-) diff --git a/RUFAS/biophysical/animal/animal.py b/RUFAS/biophysical/animal/animal.py index 279eefb17c..bbd1450661 100644 --- a/RUFAS/biophysical/animal/animal.py +++ b/RUFAS/biophysical/animal/animal.py @@ -1562,7 +1562,9 @@ def _determine_days_in_milk(self, reproduction_output_days_in_milk: int) -> int: raise ValueError("Unexpected days in milk value") def daily_reproduction_update( - self, time: RufasTime + self, + time: RufasTime, + population_ranking_indexes: list[float] | None = None, ) -> tuple[NewBornCalfValuesTypedDict | None, HerdReproductionStatistics]: """ Handles the daily reproduction state update for an animal. @@ -1597,6 +1599,8 @@ def daily_reproduction_update( dam_tbv_fat=self.genetics.TBV_fat, dam_tbv_protein=self.genetics.TBV_protein, phosphorus_for_gestation_required_for_calf=self.nutrients.phosphorus_for_gestation_required_for_calf, + population_ranking_indexes=population_ranking_indexes, + animal_ranking_index=self.genetics.ranking_index ) else: reproduction_inputs = ReproductionInputs( @@ -1607,6 +1611,8 @@ def daily_reproduction_update( days_in_pregnancy=self.days_in_pregnancy, days_in_milk=self.days_in_milk, phosphorus_for_gestation_required_for_calf=self.nutrients.phosphorus_for_gestation_required_for_calf, + population_ranking_indexes=population_ranking_indexes, + animal_ranking_index=self.genetics.ranking_index ) reproduction_outputs: ReproductionOutputs = self.reproduction.reproduction_update(reproduction_inputs, time) @@ -1638,7 +1644,7 @@ def daily_reproduction_update( return newborn_calf_config, reproduction_outputs.herd_reproduction_statistics - def daily_routines(self, time: RufasTime) -> DailyRoutinesOutput: + def daily_routines(self, time: RufasTime, population_ranking_indexes: list[float] | None) -> DailyRoutinesOutput: """ Perform daily routines for the animal, updating its status and outputs. @@ -1668,7 +1674,10 @@ def daily_routines(self, time: RufasTime) -> DailyRoutinesOutput: self.daily_growth_update(time) - newborn_calf_config, daily_routines_output.herd_reproduction_statistics = self.daily_reproduction_update(time) + newborn_calf_config, daily_routines_output.herd_reproduction_statistics = self.daily_reproduction_update( + time, + population_ranking_indexes + ) daily_routines_output.animal_status, daily_routines_output.newborn_calf_config = self.animal_life_stage_update( time @@ -2505,3 +2514,16 @@ def update_genetic_history(self, simulation_day: int) -> None: self.genetic_history[-1]["end_day"] = simulation_day else: return + + @property + def is_eligible_for_breeding(self) -> bool: + if self.animal_type is AnimalType.HEIFER_II: + return not self.is_pregnant + elif self.animal_type.is_cow: + return ( + not self.is_pregnant + and self.days_in_milk > AnimalConfig.voluntary_waiting_period + and not self.reproduction.do_not_breed + ) + else: + return False \ No newline at end of file diff --git a/RUFAS/biophysical/animal/animal_config.py b/RUFAS/biophysical/animal/animal_config.py index 2ae3d0a454..dba51891e4 100644 --- a/RUFAS/biophysical/animal/animal_config.py +++ b/RUFAS/biophysical/animal/animal_config.py @@ -195,28 +195,16 @@ class AnimalConfig: semen_type: str = "conventional" male_calf_rate_conventional_semen: float = 0.53 male_calf_rate_sexed_semen: float = 0.10 - semen_allocation: list[dict[str, float | str]] = [ - { - "lower_percentage": 0, - "upper_percentage": 25, - "semen_type": "conventional_beef", - }, - { - "lower_percentage": 25, - "upper_percentage": 50, - "semen_type": "sexed_beef", - }, - { - "lower_percentage": 50, - "upper_percentage": 75, - "semen_type": "conventional_dairy", - }, - { - "lower_percentage": 75, - "upper_percentage": 100, - "semen_type": "sexed_dairy", - }, - ] + heiferII_semen_allocation_proportions: dict[str, float] = { + "sexed_dairy": 0.5, + "conventional_dairy": 0.5, + "beef": 0.0, + } + cow_semen_allocation_proportions: dict[str, float] = { + "sexed_dairy": 0.2, + "conventional_dairy": 0.5, + "beef": 0.3, + } keep_female_calf_rate: float = 1 still_birth_rate: float = 0.065 average_gestation_length: int = 276 diff --git a/RUFAS/biophysical/animal/data_types/reproduction.py b/RUFAS/biophysical/animal/data_types/reproduction.py index 81de26f0cd..c077682895 100644 --- a/RUFAS/biophysical/animal/data_types/reproduction.py +++ b/RUFAS/biophysical/animal/data_types/reproduction.py @@ -42,6 +42,8 @@ class ReproductionInputs: days_in_pregnancy: int days_in_milk: int phosphorus_for_gestation_required_for_calf: float + population_ranking_indexes: list[float] + animal_ranking_index: float dam_tbv_fat: float | None = None dam_tbv_protein: float | None = None @@ -319,6 +321,8 @@ class ReproductionDataStream: phosphorus_for_gestation_required_for_calf: float herd_reproduction_statistics: HerdReproductionStatistics + population_ranking_indexes: list[float] + animal_ranking_index: float newborn_calf_config: NewBornCalfValuesTypedDict | None = None dam_tbv_fat: float | None = None dam_tbv_protein: float | None = None @@ -333,7 +337,6 @@ def is_milking(self) -> bool: class SemenType(Enum): - SEXED_BEEF = "sexed_beef" - CONVENTIONAL_BEEF = "conventional_beef" + BEEF = "beef" SEXED_DAIRY = "sexed_dairy" CONVENTIONAL_DAIRY = "conventional_dairy" diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index e2c92526df..247ff274b2 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -175,8 +175,8 @@ def __init__( herd_population.cows, herd_population.replacement, ) - self._assign_semen_type_for_new_heiferIIs(self.heiferIIs) - self._assign_semen_type_for_cows(self.cows) + # TODO: randomly assign embryo sex for animals that are already pregnant: use Conventional Dairy + self.allocate_animals_to_pens(time.simulation_day) self.initialize_nutrient_requirements(weather, time, available_feeds) @@ -491,8 +491,14 @@ def _perform_daily_routines_for_animals( sold_newborn_calves: list[Animal] = [] newborn_calves: list[Animal] = [] + animal_ranking_indexes: list[float] | None = None + if all([(animal.animal_type == AnimalType.HEIFER_II or animal.animal_type.is_cow) for animal in animals]): + animal_ranking_indexes: list[float] | None = [ + animal.genetics.ranking_index for animal in animals if animal.is_eligible_for_breeding + ] + for animal in animals: - animal_daily_routines_output: DailyRoutinesOutput = animal.daily_routines(time) + animal_daily_routines_output: DailyRoutinesOutput = animal.daily_routines(time, animal_ranking_indexes) self.herd_reproduction_statistics += animal_daily_routines_output.herd_reproduction_statistics if animal_daily_routines_output.animal_status == AnimalStatus.DEAD: self.herd_statistics.animals_deaths_by_stage[animal.animal_type] += 1 @@ -508,57 +514,12 @@ def _perform_daily_routines_for_animals( sold_newborn_calves.append(newborn_calf) else: newborn_calves.append(newborn_calf) - birth_year = Utility.back_track_birth_date(animal.days_born, time.current_date).year - mean_tbv_fat, mean_tbv_protein = Genetics.calculate_average_tbv( - [animal.genetics for animal in self.cows] - ) - animal.genetics.recalculate_values_at_lactation_start( - birth_year=birth_year, - animal_type=animal.animal_type, - parity=animal.calves, - group_specific_TBV_fat_mean=mean_tbv_fat, - group_specific_TBV_protein_mean=mean_tbv_protein, - ) - self._assign_semen_type_for_cow_giving_birth(animal) self._update_genetic_values_at_lactation_start(animal, time) elif animal_daily_routines_output.animal_status in [AnimalStatus.DEAD, AnimalStatus.SOLD]: sold_animals.append(animal) animal.update_genetic_history(simulation_day=time.simulation_day) return (graduated_animals, sold_animals, stillborn_newborn_calves, newborn_calves, sold_newborn_calves) - def _assign_semen_type_for_new_heiferIIs(self, new_heiferIIs: list[Animal]) -> None: - heiferII_ranking_indexes: list[float] = [heifer_II.genetics.ranking_index for heifer_II in self.heiferIIs] - for heiferII in new_heiferIIs: - ranking_index = heiferII.genetics.ranking_index - semen_type = self._find_semen_type(heiferII_ranking_indexes, ranking_index) - heiferII.reproduction.semen_type = semen_type - - def _assign_semen_type_for_cows(self, cows: list[Animal]) -> None: - cow_ranking_indexes: list[float] = [cow.genetics.ranking_index for cow in self.cows] - for cow in cows: - ranking_index = cow.genetics.ranking_index - semen_type = self._find_semen_type(cow_ranking_indexes, ranking_index) - cow.reproduction.semen_type = semen_type - - def _assign_semen_type_for_cow_giving_birth(self, cow: Animal) -> None: - cow_ranking_indexes: list[float] = [cow.genetics.ranking_index for cow in self.cows] - semen_type = self._find_semen_type(cow_ranking_indexes, cow.genetics.ranking_index) - cow.reproduction.semen_type = semen_type - - def _find_semen_type(self, population_ranking_index: list[float], animal_ranking_index: float) -> SemenType: - animal_ranking_index_percentile: float = ( - np.mean(np.array(population_ranking_index) <= animal_ranking_index) * 100 - ) - for allocation in sorted(AnimalConfig.semen_allocation, key=lambda x: x["lower_percentage"]): - lower = allocation["lower_percentage"] - upper = allocation["upper_percentage"] - if lower <= animal_ranking_index_percentile < upper or ( - upper == 100 and animal_ranking_index_percentile == 100 - ): - return SemenType(allocation["semen_type"]) - self.om.add_error("", "", {}) - raise ValueError() - def _update_genetic_values_at_lactation_start(self, animal: Animal, time: RufasTime) -> None: """ Updates the genetic values of an animal at the start of a new lactation. @@ -660,7 +621,6 @@ def daily_routines( graduated_heiferIs, sold_heiferIs, _, _, _ = self._perform_daily_routines_for_animals(time, self.heiferIs) graduated_animals += graduated_heiferIs removed_animals += sold_heiferIs - self._assign_semen_type_for_new_heiferIIs(graduated_heiferIs) graduated_heiferIIs, sold_heiferIIs, _, _, _ = self._perform_daily_routines_for_animals(time, self.heiferIIs) graduated_animals += graduated_heiferIIs diff --git a/RUFAS/biophysical/animal/reproduction/reproduction.py b/RUFAS/biophysical/animal/reproduction/reproduction.py index 94ac84fce6..64aec8ec5a 100644 --- a/RUFAS/biophysical/animal/reproduction/reproduction.py +++ b/RUFAS/biophysical/animal/reproduction/reproduction.py @@ -4,6 +4,7 @@ from math import floor from typing import Callable, Union, Any, Optional +import numpy as np from scipy.stats import truncnorm from RUFAS.biophysical.animal import animal_constants @@ -190,6 +191,8 @@ def reproduction_update(self, reproduction_inputs: ReproductionInputs, time: Ruf phosphorus_for_gestation_required_for_calf=reproduction_inputs.phosphorus_for_gestation_required_for_calf, herd_reproduction_statistics=HerdReproductionStatistics(), newborn_calf_config=None, + population_ranking_indexes=reproduction_inputs.population_ranking_indexes, + animal_ranking_index=reproduction_inputs.animal_ranking_index, ) self.reproduction_statistics.reset_daily_statistics() @@ -470,6 +473,8 @@ def cow_give_birth( dam_tbv_fat=reproduction_data_stream.dam_tbv_fat, dam_tbv_protein=reproduction_data_stream.dam_tbv_protein, ) + self.semen_type = None + self.embryo_sex = None return reproduction_data_stream @@ -1012,6 +1017,11 @@ def _perform_ai( self, reproduction_data_stream: ReproductionDataStream, simulation_day: int ) -> ReproductionDataStream: """Perform artificial insemination (AI) on the animal.""" + self.assign_semen_type( + population_ranking_index=reproduction_data_stream.population_ranking_indexes, + animal_ranking_index=reproduction_data_stream.animal_ranking_index, + animal_type=reproduction_data_stream.animal_type, + ) reproduction_data_stream.events.add_event( reproduction_data_stream.days_born, simulation_day, @@ -1044,6 +1054,7 @@ def _perform_ai( reproduction_data_stream = self._increment_successful_cow_conceptions(reproduction_data_stream) self.embryo_sex = self._determine_embryo_sex(simulation_day) else: + self.semen_type = None if reproduction_data_stream.animal_type == AnimalType.HEIFER_II: reproduction_data_stream = self._handle_failed_heifer_conception( reproduction_data_stream, simulation_day @@ -1055,9 +1066,11 @@ def _perform_ai( def _determine_embryo_sex(self, simulation_day) -> Sex: # TODO: put male calf rate into Animal Constant - if self.semen_type in [SemenType.CONVENTIONAL_DAIRY, SemenType.CONVENTIONAL_BEEF]: + if self.semen_type == SemenType.CONVENTIONAL_DAIRY: male_calf_rate = 0.5 - elif self.semen_type in [SemenType.SEXED_DAIRY, SemenType.SEXED_BEEF]: + elif self.semen_type == SemenType.SEXED_DAIRY: + male_calf_rate = 0.9 + elif self.semen_type == SemenType.BEEF: male_calf_rate = 0.9 else: print(simulation_day, self.semen_type) @@ -1144,6 +1157,7 @@ def _handle_failed_heifer_conception( std_estrus_cycle, ) # TODO: set sex to None when animal loses preg + self.semen_type = None self.embryo_sex = None return reproduction_data_stream @@ -2011,6 +2025,7 @@ def _handle_failed_cow_conception( simulation_day, f"Current repro state(s): {self.repro_state_manager}", ) + self.semen_type = None self.embryo_sex = None return reproduction_data_stream @@ -2217,3 +2232,23 @@ def _exit_ovsynch_program_early_when_first_preg_check_passed_or_estrus_detected( f" {AnimalConfig.cow_ovsynch_method}", ) return reproduction_data_stream + + def assign_semen_type( + self, population_ranking_index: list[float], animal_ranking_index: float, animal_type: AnimalType + ) -> None: + animal_ranking_index_percentile: float = ( + np.mean(np.array(population_ranking_index) <= animal_ranking_index) * 100 + ) + semen_allocation_proportions = ( + AnimalConfig.heiferII_semen_allocation_proportions + if animal_type == AnimalType.HEIFER_II + else AnimalConfig.cow_semen_allocation_proportions + ) + if animal_ranking_index_percentile > (1 - semen_allocation_proportions["sexed_dairy"]): + self.semen_type = SemenType.SEXED_DAIRY + elif animal_ranking_index_percentile > ( + 1 - semen_allocation_proportions["sexed_dairy"] - semen_allocation_proportions["conventional_dairy"] + ): + self.semen_type = SemenType.CONVENTIONAL_DAIRY + else: + self.semen_type = SemenType.BEEF From 157c3ce19c4744789b3d7d5f37d3cbefc46f2273 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 20 Apr 2026 13:07:04 +0000 Subject: [PATCH 5/6] Apply Black Formatting --- RUFAS/biophysical/animal/animal.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/RUFAS/biophysical/animal/animal.py b/RUFAS/biophysical/animal/animal.py index bbd1450661..b6a7082c69 100644 --- a/RUFAS/biophysical/animal/animal.py +++ b/RUFAS/biophysical/animal/animal.py @@ -1600,7 +1600,7 @@ def daily_reproduction_update( dam_tbv_protein=self.genetics.TBV_protein, phosphorus_for_gestation_required_for_calf=self.nutrients.phosphorus_for_gestation_required_for_calf, population_ranking_indexes=population_ranking_indexes, - animal_ranking_index=self.genetics.ranking_index + animal_ranking_index=self.genetics.ranking_index, ) else: reproduction_inputs = ReproductionInputs( @@ -1612,7 +1612,7 @@ def daily_reproduction_update( days_in_milk=self.days_in_milk, phosphorus_for_gestation_required_for_calf=self.nutrients.phosphorus_for_gestation_required_for_calf, population_ranking_indexes=population_ranking_indexes, - animal_ranking_index=self.genetics.ranking_index + animal_ranking_index=self.genetics.ranking_index, ) reproduction_outputs: ReproductionOutputs = self.reproduction.reproduction_update(reproduction_inputs, time) @@ -1675,8 +1675,7 @@ def daily_routines(self, time: RufasTime, population_ranking_indexes: list[float self.daily_growth_update(time) newborn_calf_config, daily_routines_output.herd_reproduction_statistics = self.daily_reproduction_update( - time, - population_ranking_indexes + time, population_ranking_indexes ) daily_routines_output.animal_status, daily_routines_output.newborn_calf_config = self.animal_life_stage_update( @@ -2521,9 +2520,9 @@ def is_eligible_for_breeding(self) -> bool: return not self.is_pregnant elif self.animal_type.is_cow: return ( - not self.is_pregnant - and self.days_in_milk > AnimalConfig.voluntary_waiting_period - and not self.reproduction.do_not_breed + not self.is_pregnant + and self.days_in_milk > AnimalConfig.voluntary_waiting_period + and not self.reproduction.do_not_breed ) else: - return False \ No newline at end of file + return False From d3fee642bed62f5a930f55733e0004bde91f1a45 Mon Sep 17 00:00:00 2001 From: allisterakun Date: Mon, 20 Apr 2026 13:11:53 +0000 Subject: [PATCH 6/6] Update badges on README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 79309f000b..2542731f05 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -[![Flake8](https://img.shields.io/badge/Flake8-passed-brightgreen)](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-1229%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1233%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems