diff --git a/RUFAS/biophysical/animal/animal.py b/RUFAS/biophysical/animal/animal.py index b60edcc87e..a56f6c5441 100644 --- a/RUFAS/biophysical/animal/animal.py +++ b/RUFAS/biophysical/animal/animal.py @@ -1767,9 +1767,6 @@ def daily_reproduction_update( self.milk_production.set_wood_parameters( wood_parameters["l"], wood_parameters["m"], wood_parameters["n"] ) - self.future_death_date = self.determine_future_death_date() - self._future_death_reason = animal_constants.DEATH_CULL - self.future_cull_date, self.cull_reason = self.determine_future_cull_date() self.events += reproduction_outputs.events @@ -2422,110 +2419,73 @@ def _get_cow_values(self) -> CowValuesTypedDict: parity=self.calves, ) - def determine_future_death_date(self) -> int: + def assess_removal_risk(self, percent_fresh: float, time: RufasTime) -> None: """ - Determine the future death date of the animal based on its parity. + Roll a cow's daily mortality and acute-sale risk and schedule any resulting removal. - Returns - ------- - int - Calculated future death date in simulation days. + Parameters + ---------- + percent_fresh : float + Fraction of the herd currently fresh, used to scale the daily death and + acute-sale selection probabilities. + time : RufasTime + Current simulation time, used to record the day of death or sale. - Notes + Returns ------- - [AN.ANM.1] - + None """ - if self.calves >= 4: - death_rate = AnimalConfig.parity_death_probability[3] - else: - death_rate = AnimalConfig.parity_death_probability[self.calves - 1] - death_rand = random() - if death_rand <= death_rate: - death_probability_upper_limit = death_probability_lower_limit = 0.0 - death_time_upper_limit = death_time_lower_limit = 0.0 - death_date_random = random() - for i in range(len(AnimalConfig.death_day_probability) - 1): - if ( - AnimalConfig.death_day_probability[i] - <= death_date_random - < AnimalConfig.death_day_probability[i + 1] - ): - death_probability_lower_limit = AnimalConfig.death_day_probability[i] - death_probability_upper_limit = AnimalConfig.death_day_probability[i + 1] - death_time_lower_limit = AnimalConfig.cull_day_count[i] - death_time_upper_limit = AnimalConfig.cull_day_count[i + 1] - n = (death_time_upper_limit - death_time_lower_limit) / ( - death_probability_upper_limit - death_probability_lower_limit - ) - return round( - death_time_lower_limit + n * (death_date_random - death_probability_lower_limit) + self.days_born - ) - return sys.maxsize + if self.future_cull_date == sys.maxsize: + if self.is_selected_for_acute_sale(percent_fresh): + self.future_cull_date = self.days_born + self.cull_reason = animal_constants.ACUTE_SALE_CULL + self.sold_at_day = time.simulation_day + return + if self.future_death_date == sys.maxsize: + if self.is_selected_for_death(percent_fresh): + self.future_death_date = self.days_born + self.cull_reason = self._future_death_reason = animal_constants.DEATH_CULL + self.dead_at_day = time.simulation_day + + def _parity_index(self) -> int: + """Return the 0-based index into a by-parity array, capping parity 4+ at the last entry.""" + return 3 if self.calves >= 4 else self.calves - 1 - def determine_future_cull_date(self) -> tuple[int, str]: + def is_selected_for_death(self, percent_fresh: float) -> bool: """ - Determine the future cull date and reason for the animal based on parity-specific probabilities. + Roll the cow's daily mortality risk. Returns ------- - tuple[int, str] - - Future cull date in simulation days. - - Reason for culling. + bool + ``True`` if the cow is selected to die, ``False`` otherwise. + """ + average_daily_death_rate = AnimalConfig.parity_death_probability[self._parity_index()] / 365 + percent_other = 1 - percent_fresh - Notes - ------- - [AN.ANM.2] + daily_death_risk_other = average_daily_death_rate / (2.55 * percent_fresh + percent_other) + daily_death_risk_fresh = 2.55 * daily_death_risk_other + daily_death_risk = daily_death_risk_fresh if self.days_in_milk < 50 else daily_death_risk_other + return random() <= daily_death_risk + + def is_selected_for_acute_sale(self, percent_fresh: float) -> bool: """ - cull_reason = "" - future_cull_date = sys.maxsize - if self.calves >= 4: - inv_cull_rate = AnimalConfig.parity_cull_probability[3] - else: - inv_cull_rate = AnimalConfig.parity_cull_probability[self.calves - 1] - cull_rand = random() - if cull_rand <= inv_cull_rate: - cull_reason_rand = random() - cull_prob = 0.0 - if cull_reason_rand <= (cull_prob := cull_prob + AnimalConfig.feet_leg_cull_probability): - cull_reason_cull_prob = AnimalConfig.feet_leg_cull_day_probability - cull_reason = animal_constants.LAMENESS_CULL - - elif cull_reason_rand <= (cull_prob := cull_prob + AnimalConfig.injury_cull_probability): - cull_reason_cull_prob = AnimalConfig.injury_cull_day_probability - cull_reason = animal_constants.INJURY_CULL - - elif cull_reason_rand <= (cull_prob := cull_prob + AnimalConfig.mastitis_cull_probability): - cull_reason_cull_prob = AnimalConfig.mastitis_cull_day_probability - cull_reason = animal_constants.MASTITIS_CULL - - elif cull_reason_rand <= (cull_prob := cull_prob + AnimalConfig.disease_cull_probability): - cull_reason_cull_prob = AnimalConfig.disease_cull_day_probability - cull_reason = animal_constants.DISEASE_CULL - - elif cull_reason_rand <= (cull_prob + AnimalConfig.udder_cull_probability): - cull_reason_cull_prob = AnimalConfig.udder_cull_day_probability - cull_reason = animal_constants.UDDER_CULL + Roll the cow's daily acute-sale (forced / involuntary) risk. - else: - cull_reason_cull_prob = AnimalConfig.unknown_cull_day_probability - cull_reason = animal_constants.UNKNOWN_CULL - - cull_time_rand = random() - cull_reason_upper_limit = cull_reason_lower_limit = cull_time_upper_limit = cull_time_lower_limit = 0.0 - for i in range(len(cull_reason_cull_prob) - 1): - if cull_reason_cull_prob[i] <= cull_time_rand < cull_reason_cull_prob[i + 1]: - cull_reason_lower_limit = cull_reason_cull_prob[i] - cull_reason_upper_limit = cull_reason_cull_prob[i + 1] - cull_time_lower_limit = AnimalConfig.cull_day_count[i] - cull_time_upper_limit = AnimalConfig.cull_day_count[i + 1] - x = (cull_time_upper_limit - cull_time_lower_limit) / (cull_reason_upper_limit - cull_reason_lower_limit) - future_cull_date = round( - cull_time_lower_limit + x * (cull_time_rand - cull_reason_lower_limit) + self.days_born - ) + Returns + ------- + bool + ``True`` if the cow is selected for an acute sale, ``False`` otherwise. + """ + average_daily_removal_rate = AnimalConfig.parity_acute_sale_probability[self._parity_index()] / 365 + percent_other = 1 - percent_fresh + + daily_removal_risk_other = average_daily_removal_rate / (2.55 * percent_fresh + percent_other) + daily_removal_risk_fresh = 2.55 * daily_removal_risk_other - return future_cull_date, cull_reason + daily_removal_risk = daily_removal_risk_fresh if self.days_in_milk < 50 else daily_removal_risk_other + return random() <= daily_removal_risk def update_pen_history(self, current_pen: int, current_day: int, animal_types_in_pen: set[AnimalType]) -> None: """ diff --git a/RUFAS/biophysical/animal/animal_config.py b/RUFAS/biophysical/animal/animal_config.py index a11a7cde68..359f9211b7 100644 --- a/RUFAS/biophysical/animal/animal_config.py +++ b/RUFAS/biophysical/animal/animal_config.py @@ -146,37 +146,10 @@ class AnimalConfig: third_pregnancy_check_loss_rate : float Pregnancy loss probability during the third pregnancy check, (unitless). parity_death_probability : list[float] - List of probabilities of death based on parity number, (unitless). - death_day_probability : list[float] - Cumulative probability of cow death as a function of days in production, (unitless). - parity_cull_probability : list[float] - List of culling probabilities based on parity number, (unitless). - cull_day_count : list[int] - List of day intervals for culling analysis, (simulation day). - feet_leg_cull_probability : float - Probability of feet and leg-related culling, (unitless). - feet_leg_cull_day_probability : list[float] - Feet and leg-related culling probability over time, (unitless). - injury_cull_probability : float - Probability of culling due to injuries, (unitless). - injury_cull_day_probability : list[float] - Cumulative distribution for injury-related culling over time, (unitless). - mastitis_cull_probability : float - Probability of culling due to mastitis, (unitless). - mastitis_cull_day_probability : list[float] - Cumulative distribution for mastitis-related culling over time, (unitless). - disease_cull_probability : float - Probability of culling due to diseases, (unitless). - disease_cull_day_probability : list[float] - Cumulative distribution for disease-related culling over time, (unitless). - udder_cull_probability : float - Probability of culling due to udder-related issues, (unitless). - udder_cull_day_probability : list[float] - Cumulative distribution for udder-related culling over time, (unitless). - unknown_cull_probability : float - Probability of culling for unknown reasons, (unitless). - unknown_cull_day_probability : list[float] - Cumulative distribution for unknown reasons of culling over time, (unitless). + Annual, parity-indexed probability that a cow dies during a given year, (unitless). + parity_acute_sale_probability : list[float] + Annual, parity-indexed probability that a cow is sold for an acute / involuntary + reason during a given year, (unitless). methane_mitigation_method : str The mitigation method applied for methane reduction, e.g., "None", (unitless). methane_mitigation_additive_amount : float @@ -268,112 +241,7 @@ class AnimalConfig: third_pregnancy_check_loss_rate: float = 0.017 parity_death_probability: list[float] = [0.039, 0.056, 0.085, 0.117] - death_day_probability: list[float] = [0, 0.18, 0.32, 0.42, 0.48, 0.54, 0.60, 0.65, 0.70, 0.77, 0.83, 0.89, 0.95, 1] - - parity_cull_probability: list[float] = [0.169, 0.233, 0.301, 0.408] - cull_day_count: list[int] = [0, 5, 15, 45, 90, 135, 180, 225, 270, 330, 380, 430, 480, 530] - feet_leg_cull_probability: float = 0.1633 - feet_leg_cull_day_probability: list[float] = [ - 0, - 0.03, - 0.08, - 0.16, - 0.25, - 0.36, - 0.48, - 0.59, - 0.69, - 0.78, - 0.85, - 0.90, - 0.95, - 1, - ] - injury_cull_probability: float = 0.2883 - injury_cull_day_probability: list[float] = [ - 0, - 0.08, - 0.18, - 0.28, - 0.38, - 0.47, - 0.56, - 0.64, - 0.71, - 0.78, - 0.85, - 0.90, - 0.95, - 1, - ] - mastitis_cull_probability: float = 0.2439 - mastitis_cull_day_probability: list[float] = [ - 0, - 0.06, - 0.12, - 0.19, - 0.30, - 0.43, - 0.56, - 0.68, - 0.78, - 0.85, - 0.90, - 0.94, - 0.97, - 1, - ] - disease_cull_probability: float = 0.1391 - disease_cull_day_probability: list[float] = [ - 0, - 0.04, - 0.12, - 0.24, - 0.34, - 0.42, - 0.50, - 0.57, - 0.64, - 0.72, - 0.81, - 0.89, - 0.95, - 1, - ] - udder_cull_probability: float = 0.0645 - udder_cull_day_probability: list[float] = [ - 0, - 0.12, - 0.24, - 0.33, - 0.41, - 0.48, - 0.55, - 0.62, - 0.68, - 0.76, - 0.82, - 0.89, - 0.95, - 1, - ] - unknown_cull_probability: float = 0.1009 - unknown_cull_day_probability: list[float] = [ - 0, - 0.05, - 0.11, - 0.18, - 0.27, - 0.37, - 0.45, - 0.54, - 0.62, - 0.70, - 0.77, - 0.84, - 0.92, - 1, - ] + parity_acute_sale_probability: list[float] = [0.169, 0.233, 0.301, 0.408] methane_model: dict[str, Any] = { "calves": "Pattanaik", @@ -515,32 +383,7 @@ def initialize_animal_config(cls) -> None: cls.third_pregnancy_check_loss_rate = animal_config_data["from_literature"]["repro"]["preg_loss_rate_3"] cls.parity_death_probability = animal_config_data["from_literature"]["culling"]["parity_death_prob"] - cls.death_day_probability = animal_config_data["from_literature"]["culling"]["death_day_prob"] - - cls.parity_cull_probability = animal_config_data["from_literature"]["culling"]["parity_cull_prob"] - cls.cull_day_count = animal_config_data["from_literature"]["culling"]["cull_day_count"] - cls.feet_leg_cull_probability = animal_config_data["from_literature"]["culling"]["feet_leg_cull"]["probability"] - cls.feet_leg_cull_day_probability = animal_config_data["from_literature"]["culling"]["feet_leg_cull"][ - "cull_day_prob" - ] - cls.injury_cull_probability = animal_config_data["from_literature"]["culling"]["injury_cull"]["probability"] - cls.injury_cull_day_probability = animal_config_data["from_literature"]["culling"]["injury_cull"][ - "cull_day_prob" - ] - cls.mastitis_cull_probability = animal_config_data["from_literature"]["culling"]["mastitis_cull"]["probability"] - cls.mastitis_cull_day_probability = animal_config_data["from_literature"]["culling"]["mastitis_cull"][ - "cull_day_prob" - ] - cls.disease_cull_probability = animal_config_data["from_literature"]["culling"]["disease_cull"]["probability"] - cls.disease_cull_day_probability = animal_config_data["from_literature"]["culling"]["disease_cull"][ - "cull_day_prob" - ] - cls.udder_cull_probability = animal_config_data["from_literature"]["culling"]["udder_cull"]["probability"] - cls.udder_cull_day_probability = animal_config_data["from_literature"]["culling"]["udder_cull"]["cull_day_prob"] - cls.unknown_cull_probability = animal_config_data["from_literature"]["culling"]["unknown_cull"]["probability"] - cls.unknown_cull_day_probability = animal_config_data["from_literature"]["culling"]["unknown_cull"][ - "cull_day_prob" - ] + cls.parity_acute_sale_probability = animal_config_data["from_literature"]["culling"]["parity_acute_sale_prob"] cls.methane_model = animal_data["methane_model"] methane_mitigation_data = animal_data["methane_mitigation"] diff --git a/RUFAS/biophysical/animal/animal_constants.py b/RUFAS/biophysical/animal/animal_constants.py index 3515ff06e5..cf905ffc77 100644 --- a/RUFAS/biophysical/animal/animal_constants.py +++ b/RUFAS/biophysical/animal/animal_constants.py @@ -96,12 +96,7 @@ HEIFER_REPRO_CULL = "culled for heifer reproductive problem" OVERSUPPLY_CULL = "culled for herd resize" DEATH_CULL = "culled for death" -LAMENESS_CULL = "culled for lameness" -INJURY_CULL = "culled for injury" -MASTITIS_CULL = "culled for mastitis" -DISEASE_CULL = "culled for disease" -UDDER_CULL = "culled for udder" -UNKNOWN_CULL = "culled for unknown" +ACUTE_SALE_CULL = "culled for acute sale" # youngstock mortality (a loss from death, not a cull) CALF_MORTALITY_LOSS = "died from pre-wean mortality" diff --git a/RUFAS/biophysical/animal/animal_module_reporter.py b/RUFAS/biophysical/animal/animal_module_reporter.py index 474fd6f8ab..995844703d 100644 --- a/RUFAS/biophysical/animal/animal_module_reporter.py +++ b/RUFAS/biophysical/animal/animal_module_reporter.py @@ -842,12 +842,7 @@ def report_herd_statistics_data(cls, herd_statistics: HerdStatistics, simulation cull_reason_stats_units = { animal_constants.DEATH_CULL: MeasurementUnits.UNITLESS, animal_constants.OVERSUPPLY_CULL: MeasurementUnits.UNITLESS, - animal_constants.LAMENESS_CULL: MeasurementUnits.UNITLESS, - animal_constants.INJURY_CULL: MeasurementUnits.UNITLESS, - animal_constants.MASTITIS_CULL: MeasurementUnits.UNITLESS, - animal_constants.DISEASE_CULL: MeasurementUnits.UNITLESS, - animal_constants.UDDER_CULL: MeasurementUnits.UNITLESS, - animal_constants.UNKNOWN_CULL: MeasurementUnits.UNITLESS, + animal_constants.ACUTE_SALE_CULL: MeasurementUnits.UNITLESS, } om.add_variable( "cull_reason_stats", diff --git a/RUFAS/biophysical/animal/data_types/herd_statistics.py b/RUFAS/biophysical/animal/data_types/herd_statistics.py index 718b2ee349..0859744fcd 100644 --- a/RUFAS/biophysical/animal/data_types/herd_statistics.py +++ b/RUFAS/biophysical/animal/data_types/herd_statistics.py @@ -267,12 +267,7 @@ def __init__(self) -> None: self.cull_reason_stats = { animal_constants.DEATH_CULL: 0, animal_constants.OVERSUPPLY_CULL: 0, - animal_constants.LAMENESS_CULL: 0, - animal_constants.INJURY_CULL: 0, - animal_constants.MASTITIS_CULL: 0, - animal_constants.DISEASE_CULL: 0, - animal_constants.UDDER_CULL: 0, - animal_constants.UNKNOWN_CULL: 0, + animal_constants.ACUTE_SALE_CULL: 0, } self.parity_culling_stats_range = {"1": 0, "2": 0, "3": 0, "4": 0, "5": 0, "greater_than_5": 0} self.num_cow_for_parity = {"1": 0, "2": 0, "3": 0, "4": 0, "5": 0, "greater_than_5": 0} @@ -281,12 +276,7 @@ def __init__(self) -> None: self.cull_reason_stats_percent = { animal_constants.DEATH_CULL: 0.0, animal_constants.OVERSUPPLY_CULL: 0.0, - animal_constants.LAMENESS_CULL: 0.0, - animal_constants.INJURY_CULL: 0.0, - animal_constants.MASTITIS_CULL: 0.0, - animal_constants.DISEASE_CULL: 0.0, - animal_constants.UDDER_CULL: 0.0, - animal_constants.UNKNOWN_CULL: 0.0, + animal_constants.ACUTE_SALE_CULL: 0.0, } self.percent_cow_for_parity = { "1": 0.0, diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 18c91519f9..699a976920 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -569,6 +569,42 @@ def _perform_daily_routines_for_animals( animal.update_genetic_history(simulation_day=time.simulation_day) return (graduated_animals, sold_animals, stillborn_newborn_calves, newborn_calves, sold_newborn_calves) + def _assess_removal_risk(self, animals: list[Animal], time: RufasTime) -> tuple[list[Animal], list[Animal]]: + """ + Assess daily removal risk for each cow and collect those removed. + + Computes the fresh fraction across all cows in the herd, then rolls death and + acute-sale risk for every cow in ``animals`` via + :meth:`Animal.assess_removal_risk`. Non-cow animals are skipped. + + Parameters + ---------- + animals : list of Animal + Animals to assess on the current day. + time : RufasTime + Current simulation time, passed through to the per-animal risk assessment. + + Returns + ------- + tuple[list[Animal], list[Animal]] + A ``(sold_cows, dead_cows)`` pair listing the cows selected for acute sale + and the cows selected to die, respectively. + """ + sold_cows: list[Animal] = [] + dead_cows: list[Animal] = [] + + all_cows = [animal for animal in self.all_animals if animal.animal_type.is_cow] + fresh_cows: list[Animal] = [cow for cow in all_cows if cow.days_in_milk < 50] + percent_fresh_cows = len(fresh_cows) / len(all_cows) if len(all_cows) > 0 else 0 + for animal in animals: + if animal.animal_type.is_cow: + animal.assess_removal_risk(percent_fresh_cows, time) + if animal.sold: + sold_cows.append(animal) + if animal.dead: + dead_cows.append(animal) + return (sold_cows, dead_cows) + 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. @@ -642,17 +678,26 @@ def _process_daily_herd_updates(self, time: RufasTime) -> DailyHerdUpdates: group_sold_newborn_calves, ) = self._perform_daily_routines_for_animals(time, animals) collect_birth_results = animal_group_name in ["heiferIIIs", "cows"] - daily_herd_updates.graduated_animals += group_graduated_animals - daily_herd_updates.removed_animals += sold_animals if collect_birth_results: daily_herd_updates.stillborn_newborn_calves += group_stillborn_newborn_calves daily_herd_updates.newborn_calves += group_newborn_calves daily_herd_updates.sold_newborn_calves += group_sold_newborn_calves if animal_group_name == "heiferIIs": daily_herd_updates.sold_heiferIIs = sold_animals + elif animal_group_name == "heiferIIIs": + sold_cows, dead_cows = self._assess_removal_risk(group_graduated_animals, time) + sold_animals.extend(sold_cows) + sold_animals.extend(dead_cows) + daily_herd_updates.sold_and_died_cows.extend(sold_cows) + daily_herd_updates.sold_and_died_cows.extend(dead_cows) elif animal_group_name == "cows": - daily_herd_updates.sold_and_died_cows = sold_animals + sold_cows, dead_cows = self._assess_removal_risk(animals, time) + sold_animals.extend(sold_cows) + sold_animals.extend(dead_cows) + daily_herd_updates.sold_and_died_cows.extend(sold_animals) + daily_herd_updates.graduated_animals += group_graduated_animals + daily_herd_updates.removed_animals += sold_animals return daily_herd_updates def _apply_daily_herd_structure_updates( diff --git a/RUFAS/input/metadata/properties/default.json b/RUFAS/input/metadata/properties/default.json index 82a77509a4..abdca2691a 100644 --- a/RUFAS/input/metadata/properties/default.json +++ b/RUFAS/input/metadata/properties/default.json @@ -621,154 +621,23 @@ }, "culling": { "type": "object", - "description": "Defines probabilities and distributions for death and six health-related reasons (feet-and-leg, injury, mastitis, disease, udder, unknown) an animal might be removed (culled) from the herd.", - "cull_day_count": { - "type": "array", - "description": "Defines breakpoints that partition the cumulative distribution function (CDF) for culling probabilities into segments. These values correspond to the 'cull_day_prob' array, allowing for more accurate definition of the CDF.The numbers in the array represent days into the lactation (days in milk).", - "properties": { - "type": "number", - "minimum": 0 - } - }, - "feet_leg_cull": { - "type": "object", - "description": "Cull probabilities due to feet-and-leg-related health issues.", - "probability": { - "type": "number", - "description": "Conditional probability that a culled (sold) animal is removed due to feet-and-leg-related issues. This probability is used to determine the reason for culling after it has been decided that the animal will be culled during the current lactation. The sum of probabilities for the six culling reasons equals 1.", - "minimum": 0, - "maximum": 1 - }, - "cull_day_prob": { - "type": "array", - "description": "Cumulative distribution function (CDF) values associated with the likelihood of feet-and-leg-related culling over time. The 'cull_day_count' array defines the segments of the CDF.", - "properties": { - "type": "number", - "minimum": 0, - "maximum": 1 - } - } - }, - "injury_cull": { - "type": "object", - "description": "Cull probabilities due to injury-related health issues.", - "probability": { - "type": "number", - "description": "Conditional probability that a culled (sold) animal is removed due to injury-related issues. This probability is used to determine the reason for culling after it has been decided that the animal will be culled during the current lactation. The sum of probabilities for the six culling reasons equals 1.", - "minimum": 0, - "maximum": 1 - }, - "cull_day_prob": { - "type": "array", - "description": "Cumulative distribution function (CDF) values associated with the likelihood of injury-related culling over time. The 'cull_day_count' array defines the segments of the CDF.", - "properties": { - "type": "number", - "minimum": 0, - "maximum": 1 - } - } - }, - "mastitis_cull": { - "type": "object", - "description": "Cull probabilities due to mastitis-related health issues.", - "probability": { - "type": "number", - "description": "Conditional probability that a culled (sold) animal is removed due to mastitis-related issues. This probability is used to determine the reason for culling after it has been decided that the animal will be culled during the current lactation. The sum of probabilities for the six culling reasons equals 1.", - "minimum": 0, - "maximum": 1 - }, - "cull_day_prob": { - "type": "array", - "description": "Cumulative distribution function (CDF) values associated with the likelihood of mastitis-related culling over time. The 'cull_day_count' array defines the segments of the CDF.", - "properties": { - "type": "number", - "minimum": 0, - "maximum": 1 - } - } - }, - "disease_cull": { - "type": "object", - "description": "Cull probabilities due to disease-related health issues.", - "probability": { - "type": "number", - "description": "Conditional probability that a culled (sold) animal is removed due to general disease-related issues. This probability is used to determine the reason for culling after it has been decided that the animal will be culled during the current lactation. The sum of probabilities for the six culling reasons equals 1.", - "minimum": 0, - "maximum": 1 - }, - "cull_day_prob": { - "type": "array", - "description": "Cumulative distribution function (CDF) values associated with the likelihood of general disease-related culling over time. The 'cull_day_count' array defines the segments of the CDF.", - "properties": { - "type": "number", - "minimum": 0, - "maximum": 1 - } - } - }, - "udder_cull": { - "type": "object", - "description": "Cull probabilities due to udder-related health issues.", - "probability": { - "type": "number", - "description": "Conditional probability that a culled (sold) animal is removed due to udder-related issues. This probability is used to determine the reason for culling after it has been decided that the animal will be culled during the current lactation. The sum of probabilities for the six culling reasons equals 1.", - "minimum": 0, - "maximum": 1 - }, - "cull_day_prob": { - "type": "array", - "description": "Cumulative distribution function (CDF) values associated with the likelihood of udder-related culling over time. The 'cull_day_count' array defines the segments of the CDF.", - "properties": { - "type": "number", - "minimum": 0, - "maximum": 1 - } - } - }, - "unknown_cull": { - "type": "object", - "description": "Cull probabilities due to other health issues.", - "probability": { - "type": "number", - "description": "Conditional probability that a culled (sold) animal is removed due to unknown reasons. This probability is used to determine the reason for culling after it has been decided that the animal will be culled during the current lactation. The sum of probabilities for the six culling reasons equals 1.", - "minimum": 0, - "maximum": 1 - }, - "cull_day_prob": { - "type": "array", - "description": "Cumulative distribution function (CDF) values associated with the likelihood of culling for unknown reasons over time. The 'cull_day_count' array defines the segments of the CDF.", - "properties": { - "type": "number", - "minimum": 0, - "maximum": 1 - } - } - }, + "description": "Defines the annual, by-parity probabilities that a cow dies or is sold for an acute (involuntary) reason. The timing of each event within a lactation, previously configured here, is now a model constant (see animal_constants.py, issue #2694).", "parity_death_prob": { "type": "array", - "description": "Death Probability, by Parity", - "properties": { - "type": "number", - "description": "Death Probability, by Parity Group -- The probability of death for cows of a single parity group (first lactation, second lactation, etc.); a separate entry should be included for 1st, 2nd, 3rd, and 4th+ parities (4 entries total)", - "minimum": 0, - "maximum": 1 - } - }, - "parity_cull_prob": { - "type": "array", - "description": "Cull Probability, by Parity", + "description": "Annual Death Probability, by Parity", "properties": { "type": "number", - "description": "Cull Probability, by Parity Group -- The probability of culling for cows of a single parity group (first lactation, second lactation, etc.); a separate entry should be included for 1st, 2nd, 3rd, and 4th+ parities (4 entries total). Culling refers to removal from the herd while alive.", + "description": "Annual Death Probability, by Parity Group -- The probability that a cow of a single parity group (first lactation, second lactation, etc.) dies during a given year; a separate entry should be included for 1st, 2nd, 3rd, and 4th+ parities (4 entries total).", "minimum": 0, "maximum": 1 } }, - "death_day_prob": { + "parity_acute_sale_prob": { "type": "array", - "description": "Cumulative distribution function (CDF) values associated with the likelihood of death over time. The 'cull_day_count' array defines the segments of the CDF.", + "description": "Annual Acute-Sale Probability, by Parity", "properties": { "type": "number", + "description": "Annual Acute-Sale Probability, by Parity Group -- The probability that a cow of a single parity group is sold for an acute (forced / involuntary) reason during a given year, regardless of whether a replacement is available; a separate entry should be included for 1st, 2nd, 3rd, and 4th+ parities (4 entries total).", "minimum": 0, "maximum": 1 } diff --git a/changelog_WIP.md b/changelog_WIP.md index 0a3f98f343..45d71fac89 100644 --- a/changelog_WIP.md +++ b/changelog_WIP.md @@ -124,3 +124,4 @@ This **WIP Changelog** records development changes in progress and not yet inclu - [3256](https://github.com/RuminantFarmSystems/RuFaS/pull/3256) - [minor change] [Branch Alignment] [NoInputChange] [NoOutputChange] Aligning `dev` branch with bug-fixing code from PR 3214 that was merged into `test`. - [3260](https://github.com/RuminantFarmSystems/RuFaS/pull/3260) - [minor change] [OutputManager] [NoInputChange] [NoOutputChange] Removes duplicative `report` naming mechanism in `OutputManager`. - [3275](https://github.com/RuminantFarmSystems/RuFaS/pull/3260) - [minor change] [E2E Testing] [NoInputChange] [NoOutputChange] Removes `deepdiff` check from the process to update e2e expected results. +- [3241](https://github.com/RuminantFarmSystems/RuFaS/pull/3241) - [minor change] [Animal] [InputChange] [OutputChange] Replaces the per-lactation culling model with annual, parity-indexed death and acute-sale probabilities rolled daily. diff --git a/input/data/animal/example_freestall_animal.json b/input/data/animal/example_freestall_animal.json index b62c3d0914..bc56673da5 100644 --- a/input/data/animal/example_freestall_animal.json +++ b/input/data/animal/example_freestall_animal.json @@ -112,34 +112,8 @@ "std_estrus_cycle_after_pgf": 2 }, "culling": { - "cull_day_count": [0, 5, 15, 45, 90, 135, 180, 225, 270, 330, 380, 430, 480, 530], - "feet_leg_cull": { - "probability": 0.1633, - "cull_day_prob": [0, 0.03, 0.08, 0.16, 0.25, 0.36, 0.48, 0.59, 0.69, 0.78, 0.85, 0.90, 0.95, 1] - }, - "injury_cull": { - "probability": 0.2883, - "cull_day_prob": [0, 0.08, 0.18, 0.28, 0.38, 0.47, 0.56, 0.64, 0.71, 0.78, 0.85, 0.90, 0.95, 1] - }, - "mastitis_cull": { - "probability": 0.2439, - "cull_day_prob": [0, 0.06, 0.12, 0.19, 0.30, 0.43, 0.56, 0.68, 0.78, 0.85, 0.90, 0.94, 0.97, 1] - }, - "disease_cull": { - "probability": 0.1391, - "cull_day_prob": [0, 0.04, 0.12, 0.24, 0.34, 0.42, 0.50, 0.57, 0.64, 0.72, 0.81, 0.89, 0.95, 1] - }, - "udder_cull": { - "probability": 0.0645, - "cull_day_prob": [0, 0.12, 0.24, 0.33, 0.41, 0.48, 0.55, 0.62, 0.68, 0.76, 0.82, 0.89, 0.95, 1] - }, - "unknown_cull": { - "probability": 0.1009, - "cull_day_prob": [0, 0.05, 0.11, 0.18, 0.27, 0.37, 0.45, 0.54, 0.62, 0.70, 0.77, 0.84, 0.92, 1] - }, "parity_death_prob": [0.039,0.056,0.085,0.117], - "parity_cull_prob": [0.169, 0.233, 0.301, 0.408], - "death_day_prob": [0, 0.18, 0.32, 0.42, 0.48, 0.54, 0.60, 0.65, 0.70, 0.77, 0.83, 0.89, 0.95, 1] + "parity_acute_sale_prob": [0.169, 0.233, 0.301, 0.408] }, "life_cycle": { "still_birth_rate": 0.065 diff --git a/input/data/animal/example_open_lot_animal.json b/input/data/animal/example_open_lot_animal.json index 66c0aaf167..7d515431b5 100644 --- a/input/data/animal/example_open_lot_animal.json +++ b/input/data/animal/example_open_lot_animal.json @@ -112,34 +112,8 @@ "std_estrus_cycle_after_pgf": 2 }, "culling": { - "cull_day_count": [0, 5, 15, 45, 90, 135, 180, 225, 270, 330, 380, 430, 480, 530], - "feet_leg_cull": { - "probability": 0.1633, - "cull_day_prob": [0, 0.03, 0.08, 0.16, 0.25, 0.36, 0.48, 0.59, 0.69, 0.78, 0.85, 0.90, 0.95, 1] - }, - "injury_cull": { - "probability": 0.2883, - "cull_day_prob": [0, 0.08, 0.18, 0.28, 0.38, 0.47, 0.56, 0.64, 0.71, 0.78, 0.85, 0.90, 0.95, 1] - }, - "mastitis_cull": { - "probability": 0.2439, - "cull_day_prob": [0, 0.06, 0.12, 0.19, 0.30, 0.43, 0.56, 0.68, 0.78, 0.85, 0.90, 0.94, 0.97, 1] - }, - "disease_cull": { - "probability": 0.1391, - "cull_day_prob": [0, 0.04, 0.12, 0.24, 0.34, 0.42, 0.50, 0.57, 0.64, 0.72, 0.81, 0.89, 0.95, 1] - }, - "udder_cull": { - "probability": 0.0645, - "cull_day_prob": [0, 0.12, 0.24, 0.33, 0.41, 0.48, 0.55, 0.62, 0.68, 0.76, 0.82, 0.89, 0.95, 1] - }, - "unknown_cull": { - "probability": 0.1009, - "cull_day_prob": [0, 0.05, 0.11, 0.18, 0.27, 0.37, 0.45, 0.54, 0.62, 0.70, 0.77, 0.84, 0.92, 1] - }, "parity_death_prob": [0.039,0.056,0.085,0.117], - "parity_cull_prob": [0.169, 0.233, 0.301, 0.408], - "death_day_prob": [0, 0.18, 0.32, 0.42, 0.48, 0.54, 0.60, 0.65, 0.70, 0.77, 0.83, 0.89, 0.95, 1] + "parity_acute_sale_prob": [0.169, 0.233, 0.301, 0.408] }, "life_cycle": { "still_birth_rate": 0.065 diff --git a/input/metadata/cross_validation/animal_cross_validation.json b/input/metadata/cross_validation/animal_cross_validation.json index d592fcdb1b..66cbb7e2db 100644 --- a/input/metadata/cross_validation/animal_cross_validation.json +++ b/input/metadata/cross_validation/animal_cross_validation.json @@ -1053,182 +1053,6 @@ } ] }, - { - "description": "Sum of all cull reason probabilities must equal 1.0", - "aliases": { - "variables": { - "feet_leg_prob": "animal.animal_config.from_literature.culling.feet_leg_cull.probability", - "injury_prob": "animal.animal_config.from_literature.culling.injury_cull.probability", - "mastitis_prob": "animal.animal_config.from_literature.culling.mastitis_cull.probability", - "disease_prob": "animal.animal_config.from_literature.culling.disease_cull.probability", - "udder_prob": "animal.animal_config.from_literature.culling.udder_cull.probability", - "unknown_prob": "animal.animal_config.from_literature.culling.unknown_cull.probability" - }, - "constants": { - "one": 1.0 - } - }, - "rules": [ - { - "left_hand": { - "aggregation": { - "operation": "sum", - "operands": [ - "feet_leg_prob", - "injury_prob", - "mastitis_prob", - "disease_prob", - "udder_prob", - "unknown_prob" - ] - } - }, - "right_hand": { - "aggregation": { - "operation": "no_op", - "operands": ["one"] - } - }, - "relationship": "equal" - } - ] - }, - { - "description": "All cull_day_prob and death_day_prob must have the same length as cull_day_count", - "aliases": { - "variables": { - "cull_day_count": "animal.animal_config.from_literature.culling.cull_day_count", - "feet_leg_cull_day_prob": "animal.animal_config.from_literature.culling.feet_leg_cull.cull_day_prob", - "injury_cull_day_prob": "animal.animal_config.from_literature.culling.injury_cull.cull_day_prob", - "mastitis_cull_day_prob": "animal.animal_config.from_literature.culling.mastitis_cull.cull_day_prob", - "disease_cull_day_prob": "animal.animal_config.from_literature.culling.disease_cull.cull_day_prob", - "udder_cull_day_prob": "animal.animal_config.from_literature.culling.udder_cull.cull_day_prob", - "unknown_cull_day_prob": "animal.animal_config.from_literature.culling.unknown_cull.cull_day_prob", - "death_day_prob": "animal.animal_config.from_literature.culling.death_day_prob" - } - }, - "rules": [ - { - "left_hand": { - "aggregation": { - "operation": "no_op", - "mode": "element_wise", - "operands": ["cull_day_count"] - } - }, - "right_hand": { - "aggregation": { - "operation": "no_op", - "mode": "element_wise", - "operands": ["feet_leg_cull_day_prob"] - } - }, - "relationship": "is_equal_length" - }, - { - "left_hand": { - "aggregation": { - "operation": "no_op", - "mode": "element_wise", - "operands": ["cull_day_count"] - } - }, - "right_hand": { - "aggregation": { - "operation": "no_op", - "mode": "element_wise", - "operands": ["injury_cull_day_prob"] - } - }, - "relationship": "is_equal_length" - }, - { - "left_hand": { - "aggregation": { - "operation": "no_op", - "mode": "element_wise", - "operands": ["cull_day_count"] - } - }, - "right_hand": { - "aggregation": { - "operation": "no_op", - "mode": "element_wise", - "operands": ["mastitis_cull_day_prob"] - } - }, - "relationship": "is_equal_length" - }, - { - "left_hand": { - "aggregation": { - "operation": "no_op", - "mode": "element_wise", - "operands": ["cull_day_count"] - } - }, - "right_hand": { - "aggregation": { - "operation": "no_op", - "mode": "element_wise", - "operands": ["disease_cull_day_prob"] - } - }, - "relationship": "is_equal_length" - }, - { - "left_hand": { - "aggregation": { - "operation": "no_op", - "mode": "element_wise", - "operands": ["cull_day_count"] - } - }, - "right_hand": { - "aggregation": { - "operation": "no_op", - "mode": "element_wise", - "operands": ["udder_cull_day_prob"] - } - }, - "relationship": "is_equal_length" - }, - { - "left_hand": { - "aggregation": { - "operation": "no_op", - "mode": "element_wise", - "operands": ["cull_day_count"] - } - }, - "right_hand": { - "aggregation": { - "operation": "no_op", - "mode": "element_wise", - "operands": ["unknown_cull_day_prob"] - } - }, - "relationship": "is_equal_length" - }, - { - "left_hand": { - "aggregation": { - "operation": "no_op", - "mode": "element_wise", - "operands": ["cull_day_count"] - } - }, - "right_hand": { - "aggregation": { - "operation": "no_op", - "mode": "element_wise", - "operands": ["death_day_prob"] - } - }, - "relationship": "is_equal_length" - } - ] - }, { "description": "The maximum days carried calf cannot be more than gestation length", "aliases": { diff --git a/tests/test_biophysical/test_animal/test_animal/test_animal.py b/tests/test_biophysical/test_animal/test_animal/test_animal.py index 0d2da131b0..463c69eab6 100644 --- a/tests/test_biophysical/test_animal/test_animal/test_animal.py +++ b/tests/test_biophysical/test_animal/test_animal/test_animal.py @@ -2356,10 +2356,6 @@ def test_daily_reproduction_update(mock_lactating_cow: Animal, mocker: MockerFix animal.animal_type = AnimalType.HEIFER_II mock_determine_days_in_milk = mocker.patch.object(animal, "_determine_days_in_milk", return_value=3) mock_set_wood_parameters = mocker.patch.object(MilkProduction, "set_wood_parameters") - mock_determine_future_death_date = mocker.patch.object(animal, "determine_future_death_date", return_value=3) - mock_determine_future_cull_date = mocker.patch.object( - animal, "determine_future_cull_date", return_value=(3, "test") - ) mock_get_wood_parameters = mocker.patch.object( LactationCurve, "get_wood_parameters", return_value={"l": 10.2, "m": 41.2, "n": 41.8} ) @@ -2384,7 +2380,8 @@ def test_daily_reproduction_update(mock_lactating_cow: Animal, mocker: MockerFix ), ) mocker.patch.object(AnimalType, "is_cow", new_callable=PropertyMock, return_value=True) - mocker.patch.object(Animal, "calves", new_callable=PropertyMock, return_value=100) + # First calving (parity 1) triggers the initial removal-risk assessment. + mocker.patch.object(Animal, "calves", new_callable=PropertyMock, return_value=1) mocker.patch.object(Animal, "calving_interval_history", new_callable=PropertyMock, return_value=[100]) mocker.patch.object(AnimalEvents, "get_most_recent_date", return_value=2) result, _ = animal.daily_reproduction_update(MagicMock(RufasTime)) @@ -2393,11 +2390,7 @@ def test_daily_reproduction_update(mock_lactating_cow: Animal, mocker: MockerFix mock_get_wood_parameters.assert_called_once() mock_set_wood_parameters.assert_called_once() mock_determine_days_in_milk.assert_called_once() - mock_determine_future_cull_date.assert_called_once() - mock_determine_future_death_date.assert_called_once() - assert animal.future_cull_date == 3 - assert animal.cull_reason == "test" assert animal.days_in_milk == 3 assert animal.body_weight == 10 assert animal.days_in_pregnancy == 12 @@ -3039,22 +3032,21 @@ def test_get_cow_values(mock_lactating_cow: Animal) -> None: assert mock_lactating_cow._get_cow_values() == expected -def test_determine_future_death_date_no_death(mock_lactating_cow: Animal, mocker: MockerFixture) -> None: +def test_will_die_tomorrow_no_death(mock_lactating_cow: Animal, mocker: MockerFixture) -> None: animal = mock_lactating_cow animal.calves = 1 animal.days_born = 150 mocker.patch("RUFAS.biophysical.animal.animal.random", return_value=0.95) - result = animal.determine_future_death_date() - assert result == sys.maxsize + assert animal.is_selected_for_death(percent_fresh=0.15) is False -def test_determine_future_death_date_with_death(mock_lactating_cow: Animal, mocker: MockerFixture) -> None: +def test_will_die_tomorrow_with_death(mock_lactating_cow: Animal, mocker: MockerFixture) -> None: + """A roll below the daily death rate (annual parity probability / 365) selects the cow.""" animal = mock_lactating_cow animal.calves = 5 - animal.days_born = 12 - mocker.patch("RUFAS.biophysical.animal.animal.random", return_value=0.0005) - result = animal.determine_future_death_date() - assert result == 12 + # daily death rate = parity_death_probability[3] (0.117) / 365 ~= 0.00032. + mocker.patch("RUFAS.biophysical.animal.animal.random", return_value=0.0001) + assert animal.is_selected_for_death(percent_fresh=0.15) is True def test_setup_calf_mortality_disabled_when_rate_zero(mock_calf: Animal, mocker: MockerFixture) -> None: @@ -3217,73 +3209,19 @@ def test_setup_heifer_mortality_not_committed_when_day_already_passed( assert animal._future_death_date is None -def patch_random_first_call(mocker: MockerFixture, first_value: float, second_value: float) -> None: - called = False - - def side_effect() -> float: - nonlocal called - if not called: - called = True - return first_value - return second_value - - mocker.patch("RUFAS.biophysical.animal.animal.random", side_effect=side_effect) - - -def test_determine_future_cull_date_feet_leg(mock_lactating_cow: Animal, mocker: MockerFixture) -> None: - mock_lactating_cow.calves = 1 - mock_lactating_cow.days_born = 150 - patch_random_first_call(mocker, 0.05, 0.05) - result = mock_lactating_cow.determine_future_cull_date() - assert result == (159, animal_constants.LAMENESS_CULL) - - -def test_determine_future_cull_date_injury(mock_lactating_cow: Animal, mocker: MockerFixture) -> None: - mock_lactating_cow.calves = 6 - mock_lactating_cow.days_born = 150 - patch_random_first_call(mocker, 0.05, 0.25) - result = mock_lactating_cow.determine_future_cull_date() - assert result == (186, animal_constants.INJURY_CULL) - - -def test_determine_future_cull_date_mastitis(mock_lactating_cow: Animal, mocker: MockerFixture) -> None: - mock_lactating_cow.calves = 1 - mock_lactating_cow.days_born = 150 - patch_random_first_call(mocker, 0.05, 0.46) - result = mock_lactating_cow.determine_future_cull_date() - assert result == (295, animal_constants.MASTITIS_CULL) - - -def test_determine_future_cull_date_disease(mock_lactating_cow: Animal, mocker: MockerFixture) -> None: - mock_lactating_cow.calves = 1 - mock_lactating_cow.days_born = 150 - patch_random_first_call(mocker, 0.05, 0.7) - result = mock_lactating_cow.determine_future_cull_date() - assert result == (465, animal_constants.DISEASE_CULL) - - -def test_determine_future_cull_date_udder(mock_lactating_cow: Animal, mocker: MockerFixture) -> None: - mock_lactating_cow.calves = 1 - mock_lactating_cow.days_born = 150 - patch_random_first_call(mocker, 0.05, 0.85) - result = mock_lactating_cow.determine_future_cull_date() - assert result == (551, animal_constants.UDDER_CULL) - - -def test_determine_future_cull_date_unknown(mock_lactating_cow: Animal, mocker: MockerFixture) -> None: - mock_lactating_cow.calves = 1 - mock_lactating_cow.days_born = 150 - patch_random_first_call(mocker, 0.05, 0.9) - result = mock_lactating_cow.determine_future_cull_date() - assert result == (618, animal_constants.UNKNOWN_CULL) +def test_will_be_sold_tomorrow_with_acute_sale(mock_lactating_cow: Animal, mocker: MockerFixture) -> None: + """A roll below the daily acute-sale rate (annual parity probability / 365) selects the cow.""" + animal = mock_lactating_cow + animal.calves = 1 + # daily acute-sale rate = parity_acute_sale_probability[0] (0.169) / 365 ~= 0.00046. + mocker.patch("RUFAS.biophysical.animal.animal.random", return_value=0.0001) + assert animal.is_selected_for_acute_sale(percent_fresh=0.15) is True -def test_determine_future_cull_date_no_cull(mock_lactating_cow: Animal, mocker: MockerFixture) -> None: +def test_will_be_sold_tomorrow_no_acute_sale(mock_lactating_cow: Animal, mocker: MockerFixture) -> None: mock_lactating_cow.calves = 1 - mock_lactating_cow.days_born = 150 mocker.patch("RUFAS.biophysical.animal.animal.random", return_value=0.95) - result = mock_lactating_cow.determine_future_cull_date() - assert result == (sys.maxsize, "") + assert mock_lactating_cow.is_selected_for_acute_sale(percent_fresh=0.15) is False def test_set_nutrient_standard() -> None: diff --git a/tests/test_biophysical/test_animal/test_animal/test_animal_config.py b/tests/test_biophysical/test_animal/test_animal/test_animal_config.py index 633ac25e0d..9e0a702e01 100644 --- a/tests/test_biophysical/test_animal/test_animal/test_animal_config.py +++ b/tests/test_biophysical/test_animal/test_animal/test_animal_config.py @@ -130,34 +130,8 @@ def _make_base_animal_config(repro_sub_protocol: str, heifer_repro_method: str) "std_estrus_cycle_after_pgf": 2, }, "culling": { - "cull_day_count": [0, 5, 15, 45, 90, 135, 180, 225, 270, 330, 380, 430, 480, 530], - "feet_leg_cull": { - "probability": 0.1633, - "cull_day_prob": [0, 0.03, 0.08, 0.16, 0.25, 0.36, 0.48, 0.59, 0.69, 0.78, 0.85, 0.90, 0.95, 1], - }, - "injury_cull": { - "probability": 0.2883, - "cull_day_prob": [0, 0.08, 0.18, 0.28, 0.38, 0.47, 0.56, 0.64, 0.71, 0.78, 0.85, 0.90, 0.95, 1], - }, - "mastitis_cull": { - "probability": 0.2439, - "cull_day_prob": [0, 0.06, 0.12, 0.19, 0.30, 0.43, 0.56, 0.68, 0.78, 0.85, 0.90, 0.94, 0.97, 1], - }, - "disease_cull": { - "probability": 0.1391, - "cull_day_prob": [0, 0.04, 0.12, 0.24, 0.34, 0.42, 0.50, 0.57, 0.64, 0.72, 0.81, 0.89, 0.95, 1], - }, - "udder_cull": { - "probability": 0.0645, - "cull_day_prob": [0, 0.12, 0.24, 0.33, 0.41, 0.48, 0.55, 0.62, 0.68, 0.76, 0.82, 0.89, 0.95, 1], - }, - "unknown_cull": { - "probability": 0.1009, - "cull_day_prob": [0, 0.05, 0.11, 0.18, 0.27, 0.37, 0.45, 0.54, 0.62, 0.70, 0.77, 0.84, 0.92, 1], - }, "parity_death_prob": [0.039, 0.056, 0.085, 0.117], - "parity_cull_prob": [0.169, 0.233, 0.301, 0.408], - "death_day_prob": [0, 0.18, 0.32, 0.42, 0.48, 0.54, 0.60, 0.65, 0.70, 0.77, 0.83, 0.89, 0.95, 1], + "parity_acute_sale_prob": [0.169, 0.233, 0.301, 0.408], }, "life_cycle": {"still_birth_rate": 0.065}, }, @@ -411,34 +385,8 @@ def test_initialize_animal_config_adds_warning_when_third_check_after_or_on_dryo "std_estrus_cycle_after_pgf": 2, }, "culling": { - "cull_day_count": [0, 5, 15, 45, 90, 135, 180, 225, 270, 330, 380, 430, 480, 530], - "feet_leg_cull": { - "probability": 0.1633, - "cull_day_prob": [0, 0.03, 0.08, 0.16, 0.25, 0.36, 0.48, 0.59, 0.69, 0.78, 0.85, 0.90, 0.95, 1], - }, - "injury_cull": { - "probability": 0.2883, - "cull_day_prob": [0, 0.08, 0.18, 0.28, 0.38, 0.47, 0.56, 0.64, 0.71, 0.78, 0.85, 0.90, 0.95, 1], - }, - "mastitis_cull": { - "probability": 0.2439, - "cull_day_prob": [0, 0.06, 0.12, 0.19, 0.30, 0.43, 0.56, 0.68, 0.78, 0.85, 0.90, 0.94, 0.97, 1], - }, - "disease_cull": { - "probability": 0.1391, - "cull_day_prob": [0, 0.04, 0.12, 0.24, 0.34, 0.42, 0.50, 0.57, 0.64, 0.72, 0.81, 0.89, 0.95, 1], - }, - "udder_cull": { - "probability": 0.0645, - "cull_day_prob": [0, 0.12, 0.24, 0.33, 0.41, 0.48, 0.55, 0.62, 0.68, 0.76, 0.82, 0.89, 0.95, 1], - }, - "unknown_cull": { - "probability": 0.1009, - "cull_day_prob": [0, 0.05, 0.11, 0.18, 0.27, 0.37, 0.45, 0.54, 0.62, 0.70, 0.77, 0.84, 0.92, 1], - }, "parity_death_prob": [0.039, 0.056, 0.085, 0.117], - "parity_cull_prob": [0.169, 0.233, 0.301, 0.408], - "death_day_prob": [0, 0.18, 0.32, 0.42, 0.48, 0.54, 0.60, 0.65, 0.70, 0.77, 0.83, 0.89, 0.95, 1], + "parity_acute_sale_prob": [0.169, 0.233, 0.301, 0.408], }, "life_cycle": {"still_birth_rate": 0.065}, }, diff --git a/tests/test_biophysical/test_animal/test_animal_module_reporter/test_animal_module_reporter.py b/tests/test_biophysical/test_animal/test_animal_module_reporter/test_animal_module_reporter.py index 8f6630b041..675da98c3c 100644 --- a/tests/test_biophysical/test_animal/test_animal_module_reporter/test_animal_module_reporter.py +++ b/tests/test_biophysical/test_animal/test_animal_module_reporter/test_animal_module_reporter.py @@ -1106,7 +1106,7 @@ def test_report_sold_animal_information(mocker: MockerFixture) -> None: animal_type="LacCow", sold_at_day=123, body_weight=456.78, - cull_reason=animal_constants.UDDER_CULL, + cull_reason=animal_constants.ACUTE_SALE_CULL, days_in_milk=18, parity=2, genetic_history="", @@ -1126,7 +1126,7 @@ def test_report_sold_animal_information(mocker: MockerFixture) -> None: animal_type="DryCow", sold_at_day=123, body_weight=456.78, - cull_reason=animal_constants.LAMENESS_CULL, + cull_reason=animal_constants.ACUTE_SALE_CULL, days_in_milk=0, parity=3, genetic_history="", diff --git a/tests/test_biophysical/test_animal/test_data_types/test_herd_statistics.py b/tests/test_biophysical/test_animal/test_data_types/test_herd_statistics.py index 78f71ba185..7c68a568b6 100644 --- a/tests/test_biophysical/test_animal/test_data_types/test_herd_statistics.py +++ b/tests/test_biophysical/test_animal/test_data_types/test_herd_statistics.py @@ -235,7 +235,7 @@ def test_reset_cull_reason_stats(herd_statistics: HerdStatistics) -> None: """Test that reset_cull_reason_stats resets cull reason-based attributes correctly.""" # Set non-zero values herd_statistics.cull_reason_stats[animal_constants.DEATH_CULL] = 3 - herd_statistics.cull_reason_stats_percent[animal_constants.LAMENESS_CULL] = 40.5 + herd_statistics.cull_reason_stats_percent[animal_constants.ACUTE_SALE_CULL] = 40.5 herd_statistics.reset_cull_reason_stats() diff --git a/tests/test_biophysical/test_animal/test_herd_manager/pytest_fixtures.py b/tests/test_biophysical/test_animal/test_herd_manager/pytest_fixtures.py index 3604985a44..30c6f03f32 100644 --- a/tests/test_biophysical/test_animal/test_herd_manager/pytest_fixtures.py +++ b/tests/test_biophysical/test_animal/test_herd_manager/pytest_fixtures.py @@ -141,34 +141,8 @@ def animal_json() -> dict[str, Any]: "std_estrus_cycle_after_pgf": 2, }, "culling": { - "cull_day_count": [0, 5, 15, 45, 90, 135, 180, 225, 270, 330, 380, 430, 480, 530], - "feet_leg_cull": { - "probability": 0.1633, - "cull_day_prob": [0, 0.03, 0.08, 0.16, 0.25, 0.36, 0.48, 0.59, 0.69, 0.78, 0.85, 0.90, 0.95, 1], - }, - "injury_cull": { - "probability": 0.2883, - "cull_day_prob": [0, 0.08, 0.18, 0.28, 0.38, 0.47, 0.56, 0.64, 0.71, 0.78, 0.85, 0.90, 0.95, 1], - }, - "mastitis_cull": { - "probability": 0.2439, - "cull_day_prob": [0, 0.06, 0.12, 0.19, 0.30, 0.43, 0.56, 0.68, 0.78, 0.85, 0.90, 0.94, 0.97, 1], - }, - "disease_cull": { - "probability": 0.1391, - "cull_day_prob": [0, 0.04, 0.12, 0.24, 0.34, 0.42, 0.50, 0.57, 0.64, 0.72, 0.81, 0.89, 0.95, 1], - }, - "udder_cull": { - "probability": 0.0645, - "cull_day_prob": [0, 0.12, 0.24, 0.33, 0.41, 0.48, 0.55, 0.62, 0.68, 0.76, 0.82, 0.89, 0.95, 1], - }, - "unknown_cull": { - "probability": 0.1009, - "cull_day_prob": [0, 0.05, 0.11, 0.18, 0.27, 0.37, 0.45, 0.54, 0.62, 0.70, 0.77, 0.84, 0.92, 1], - }, "parity_death_prob": [0.039, 0.056, 0.085, 0.117], - "parity_cull_prob": [0.169, 0.233, 0.301, 0.408], - "death_day_prob": [0, 0.18, 0.32, 0.42, 0.48, 0.54, 0.60, 0.65, 0.70, 0.77, 0.83, 0.89, 0.95, 1], + "parity_acute_sale_prob": [0.169, 0.233, 0.301, 0.408], }, "life_cycle": {"still_birth_rate": 0.065}, }, diff --git a/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_daily_routines.py b/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_daily_routines.py index ad29de3569..e9c95a75a0 100644 --- a/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_daily_routines.py +++ b/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_daily_routines.py @@ -748,7 +748,7 @@ def test_daily_routines(herd_manager: HerdManager, mock_herd: dict[str, list[Ani mock_update_sold_animal_statistics.assert_called_once_with( sold_newborn_calves=[], sold_heiferIIs=sold_heiferIIs, - sold_and_died_cows=sold_and_died_cows, + sold_and_died_cows=graduated_heiferIIIs + sold_and_died_cows, ) assert mock_check_if_cows_need_to_be_sold.call_count == 0 assert mock_check_if_replacement_heifers_needed.call_count == 0 diff --git a/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_herd_statistics.py b/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_herd_statistics.py index 830a4b1768..9ea03e6d39 100644 --- a/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_herd_statistics.py +++ b/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_herd_statistics.py @@ -176,23 +176,13 @@ def test_calculate_cow_percentages(herd_manager: HerdManager, mock_herd: dict[st { animal_constants.DEATH_CULL: 0, animal_constants.OVERSUPPLY_CULL: 0, - animal_constants.LAMENESS_CULL: 0, - animal_constants.INJURY_CULL: 0, - animal_constants.MASTITIS_CULL: 0, - animal_constants.DISEASE_CULL: 0, - animal_constants.UDDER_CULL: 0, - animal_constants.UNKNOWN_CULL: 0, + animal_constants.ACUTE_SALE_CULL: 0, }, 0, { animal_constants.DEATH_CULL: 0.0, animal_constants.OVERSUPPLY_CULL: 0.0, - animal_constants.LAMENESS_CULL: 0.0, - animal_constants.INJURY_CULL: 0.0, - animal_constants.MASTITIS_CULL: 0.0, - animal_constants.DISEASE_CULL: 0.0, - animal_constants.UDDER_CULL: 0.0, - animal_constants.UNKNOWN_CULL: 0.0, + animal_constants.ACUTE_SALE_CULL: 0.0, }, ), # 2. One reason has all culls, matches exit_num -> 100% that reason @@ -200,23 +190,13 @@ def test_calculate_cow_percentages(herd_manager: HerdManager, mock_herd: dict[st { animal_constants.DEATH_CULL: 5, animal_constants.OVERSUPPLY_CULL: 0, - animal_constants.LAMENESS_CULL: 0, - animal_constants.INJURY_CULL: 0, - animal_constants.MASTITIS_CULL: 0, - animal_constants.DISEASE_CULL: 0, - animal_constants.UDDER_CULL: 0, - animal_constants.UNKNOWN_CULL: 0, + animal_constants.ACUTE_SALE_CULL: 0, }, 5, { animal_constants.DEATH_CULL: 100.0, animal_constants.OVERSUPPLY_CULL: 0.0, - animal_constants.LAMENESS_CULL: 0.0, - animal_constants.INJURY_CULL: 0.0, - animal_constants.MASTITIS_CULL: 0.0, - animal_constants.DISEASE_CULL: 0.0, - animal_constants.UDDER_CULL: 0.0, - animal_constants.UNKNOWN_CULL: 0.0, + animal_constants.ACUTE_SALE_CULL: 0.0, }, ), # 3. Multiple reasons evenly split @@ -225,23 +205,13 @@ def test_calculate_cow_percentages(herd_manager: HerdManager, mock_herd: dict[st { animal_constants.DEATH_CULL: 5, animal_constants.OVERSUPPLY_CULL: 5, - animal_constants.LAMENESS_CULL: 0, - animal_constants.INJURY_CULL: 0, - animal_constants.MASTITIS_CULL: 0, - animal_constants.DISEASE_CULL: 0, - animal_constants.UDDER_CULL: 0, - animal_constants.UNKNOWN_CULL: 0, + animal_constants.ACUTE_SALE_CULL: 0, }, 10, { animal_constants.DEATH_CULL: 50.0, animal_constants.OVERSUPPLY_CULL: 50.0, - animal_constants.LAMENESS_CULL: 0.0, - animal_constants.INJURY_CULL: 0.0, - animal_constants.MASTITIS_CULL: 0.0, - animal_constants.DISEASE_CULL: 0.0, - animal_constants.UDDER_CULL: 0.0, - animal_constants.UNKNOWN_CULL: 0.0, + animal_constants.ACUTE_SALE_CULL: 0.0, }, ), # 4. Partial distribution @@ -250,23 +220,13 @@ def test_calculate_cow_percentages(herd_manager: HerdManager, mock_herd: dict[st { animal_constants.DEATH_CULL: 3, animal_constants.OVERSUPPLY_CULL: 2, - animal_constants.LAMENESS_CULL: 0, - animal_constants.INJURY_CULL: 0, - animal_constants.MASTITIS_CULL: 0, - animal_constants.DISEASE_CULL: 0, - animal_constants.UDDER_CULL: 0, - animal_constants.UNKNOWN_CULL: 0, + animal_constants.ACUTE_SALE_CULL: 0, }, 10, { animal_constants.DEATH_CULL: 30.0, animal_constants.OVERSUPPLY_CULL: 20.0, - animal_constants.LAMENESS_CULL: 0.0, - animal_constants.INJURY_CULL: 0.0, - animal_constants.MASTITIS_CULL: 0.0, - animal_constants.DISEASE_CULL: 0.0, - animal_constants.UDDER_CULL: 0.0, - animal_constants.UNKNOWN_CULL: 0.0, + animal_constants.ACUTE_SALE_CULL: 0.0, }, ), # 5. Non-zero exit, some reasons zero @@ -276,23 +236,13 @@ def test_calculate_cow_percentages(herd_manager: HerdManager, mock_herd: dict[st { animal_constants.DEATH_CULL: 2, animal_constants.OVERSUPPLY_CULL: 0, - animal_constants.LAMENESS_CULL: 0, - animal_constants.INJURY_CULL: 0, - animal_constants.MASTITIS_CULL: 0, - animal_constants.DISEASE_CULL: 8, - animal_constants.UDDER_CULL: 0, - animal_constants.UNKNOWN_CULL: 0, + animal_constants.ACUTE_SALE_CULL: 8, }, 10, { animal_constants.DEATH_CULL: 20.0, animal_constants.OVERSUPPLY_CULL: 0.0, - animal_constants.LAMENESS_CULL: 0.0, - animal_constants.INJURY_CULL: 0.0, - animal_constants.MASTITIS_CULL: 0.0, - animal_constants.DISEASE_CULL: 80.0, - animal_constants.UDDER_CULL: 0.0, - animal_constants.UNKNOWN_CULL: 0.0, + animal_constants.ACUTE_SALE_CULL: 80.0, }, ), ], @@ -540,12 +490,7 @@ def test_update_sold_and_died_cow_statistics( """Unit test for _update_sold_and_died_cow_statistics()""" cull_reasons = [ animal_constants.OVERSUPPLY_CULL, - animal_constants.LAMENESS_CULL, - animal_constants.INJURY_CULL, - animal_constants.MASTITIS_CULL, - animal_constants.DISEASE_CULL, - animal_constants.UDDER_CULL, - animal_constants.UNKNOWN_CULL, + animal_constants.ACUTE_SALE_CULL, ] num_sold_cows, num_dead_cows = randint(1, 100), randint(1, 100) @@ -612,12 +557,7 @@ def test_update_sold_and_died_cow_statistics( current_cull_reason_stats = { animal_constants.DEATH_CULL: randint(0, num_total_sold_and_died_cows), animal_constants.OVERSUPPLY_CULL: randint(0, num_total_sold_and_died_cows), - animal_constants.LAMENESS_CULL: randint(0, num_total_sold_and_died_cows), - animal_constants.INJURY_CULL: randint(0, num_total_sold_and_died_cows), - animal_constants.MASTITIS_CULL: randint(0, num_total_sold_and_died_cows), - animal_constants.DISEASE_CULL: randint(0, num_total_sold_and_died_cows), - animal_constants.UDDER_CULL: randint(0, num_total_sold_and_died_cows), - animal_constants.UNKNOWN_CULL: randint(0, num_total_sold_and_died_cows), + animal_constants.ACUTE_SALE_CULL: randint(0, num_total_sold_and_died_cows), } herd_manager.herd_statistics.cull_reason_stats = current_cull_reason_stats expected_cull_reason_stats = { @@ -625,18 +565,8 @@ def test_update_sold_and_died_cow_statistics( + len([cow for cow in sold_and_died_cows if cow.cull_reason == animal_constants.DEATH_CULL]), animal_constants.OVERSUPPLY_CULL: current_cull_reason_stats[animal_constants.OVERSUPPLY_CULL] + len([cow for cow in sold_and_died_cows if cow.cull_reason == animal_constants.OVERSUPPLY_CULL]), - animal_constants.LAMENESS_CULL: current_cull_reason_stats[animal_constants.LAMENESS_CULL] - + len([cow for cow in sold_and_died_cows if cow.cull_reason == animal_constants.LAMENESS_CULL]), - animal_constants.INJURY_CULL: current_cull_reason_stats[animal_constants.INJURY_CULL] - + len([cow for cow in sold_and_died_cows if cow.cull_reason == animal_constants.INJURY_CULL]), - animal_constants.MASTITIS_CULL: current_cull_reason_stats[animal_constants.MASTITIS_CULL] - + len([cow for cow in sold_and_died_cows if cow.cull_reason == animal_constants.MASTITIS_CULL]), - animal_constants.DISEASE_CULL: current_cull_reason_stats[animal_constants.DISEASE_CULL] - + len([cow for cow in sold_and_died_cows if cow.cull_reason == animal_constants.DISEASE_CULL]), - animal_constants.UDDER_CULL: current_cull_reason_stats[animal_constants.UDDER_CULL] - + len([cow for cow in sold_and_died_cows if cow.cull_reason == animal_constants.UDDER_CULL]), - animal_constants.UNKNOWN_CULL: current_cull_reason_stats[animal_constants.UNKNOWN_CULL] - + len([cow for cow in sold_and_died_cows if cow.cull_reason == animal_constants.UNKNOWN_CULL]), + animal_constants.ACUTE_SALE_CULL: current_cull_reason_stats[animal_constants.ACUTE_SALE_CULL] + + len([cow for cow in sold_and_died_cows if cow.cull_reason == animal_constants.ACUTE_SALE_CULL]), } current_sold_cow_num = randint(0, current_cow_herd_exit_num)