From 8855e85c19c8ae1e2479c9205cb8c78923dc91af Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Mon, 20 Oct 2025 21:19:33 +0900 Subject: [PATCH 001/107] Initial changes --- .../animal/animal_module_reporter.py | 2 +- .../animal/data_types/herd_statistics.py | 6 +-- RUFAS/biophysical/animal/herd_manager.py | 51 ++++++++++--------- .../data/animal/example_freestall_animal.json | 3 ++ .../test_herd_manager_daily_routines.py | 2 +- 5 files changed, 35 insertions(+), 29 deletions(-) diff --git a/RUFAS/biophysical/animal/animal_module_reporter.py b/RUFAS/biophysical/animal/animal_module_reporter.py index 63ee7692ee..5c1c285f84 100644 --- a/RUFAS/biophysical/animal/animal_module_reporter.py +++ b/RUFAS/biophysical/animal/animal_module_reporter.py @@ -590,7 +590,7 @@ def report_herd_statistics_data(cls, herd_statistics: HerdStatistics, simulation } om.add_variable( "sold_heiferIII_oversupply_num", - herd_statistics.sold_heiferIII_oversupply_num, + herd_statistics.sold_cow_oversupply_num, dict(info_map, **{"units": MeasurementUnits.ANIMALS}), ) om.add_variable( diff --git a/RUFAS/biophysical/animal/data_types/herd_statistics.py b/RUFAS/biophysical/animal/data_types/herd_statistics.py index a624f544a0..562064a855 100644 --- a/RUFAS/biophysical/animal/data_types/herd_statistics.py +++ b/RUFAS/biophysical/animal/data_types/herd_statistics.py @@ -47,7 +47,7 @@ class HerdStatistics: Number of stillborn calves during a specific period, (unitless). sold_calf_num : int Number of calves sold during a specific period, (unitless). - sold_heiferIII_oversupply_num : int + sold_cow_oversupply_num : int Number of surplus "Heifer III" animals sold, (unitless). bought_heifer_num : int Number of heifers purchased during a specific period, (unitless). @@ -177,7 +177,7 @@ class HerdStatistics: stillborn_calf_num = 0 sold_calf_num = 0 - sold_heiferIII_oversupply_num = 0 + sold_cow_oversupply_num = 0 bought_heifer_num = 0 sold_heiferII_num = 0 cow_herd_exit_num = 0 @@ -312,7 +312,7 @@ def reset_daily_stats(self) -> None: self.stillborn_calf_num = 0 self.sold_calf_num = 0 - self.sold_heiferIII_oversupply_num = 0 + self.sold_cow_oversupply_num = 0 self.bought_heifer_num = 0 self.sold_heiferII_num = 0 self.cow_herd_exit_num = 0 diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 5602c728aa..00d0005f46 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -126,6 +126,7 @@ def __init__( self.herd_statistics = HerdStatistics() self.herd_statistics.herd_num = animal_config_data["herd_information"]["herd_num"] + self.adjustment_period = animal_config_data["herd_information"]["herd_size_adjustment_period"] self.herd_reproduction_statistics = HerdReproductionStatistics() self.housing = animal_config_data["housing"] @@ -589,18 +590,19 @@ def daily_routines( self._update_stillborn_calf_statistics(stillborn_newborn_calves) - removed_animals += self._check_if_heifers_need_to_be_sold(simulation_day=time.simulation_day) - newly_added_animals = self._check_if_replacement_heifers_needed(time=time) - self._update_herd_structure( - graduated_animals=graduated_animals, - newborn_calves=newborn_calves, - newly_added_animals=newly_added_animals, - removed_animals=removed_animals, - available_feeds=available_feeds, - current_day_conditions=weather.get_current_day_conditions(time), - total_inventory=total_inventory, - simulation_day=time.simulation_day, - ) + if time.simulation_day > 0 and time.simulation_day % 60 == 0: + removed_animals += self._check_if_heifers_need_to_be_sold(simulation_day=time.simulation_day) + newly_added_animals = self._check_if_replacement_heifers_needed(time=time) + self._update_herd_structure( + graduated_animals=graduated_animals, + newborn_calves=newborn_calves, + newly_added_animals=newly_added_animals, + removed_animals=removed_animals, + available_feeds=available_feeds, + current_day_conditions=weather.get_current_day_conditions(time), + total_inventory=total_inventory, + simulation_day=time.simulation_day, + ) self.record_pen_history(time.simulation_day) enteric_methane_emission_by_pen: dict[str, float] = {} @@ -691,25 +693,26 @@ def _check_if_heifers_need_to_be_sold( """ animals_removed: list[Animal] = [] while ( - self.current_herd_size > self.herd_statistics.herd_num * animal_constants.SELLING_THRESHOLD + self.current_herd_size > self.herd_statistics.herd_num * animal_constants.SELLING_THRESHOLD # TODO: Change to user input and len(self.heiferIIIs) > 0 ): - removed_heiferIII = self.heiferIIIs.pop() - animals_removed.append(removed_heiferIII) - removed_heiferIII.sold_at_day = simulation_day + self.cows.sort(key=lambda cow: cow.milk_production.daily_milk_produced) + removed_cow = self.cows.pop() + removed_cow.sold_at_day = simulation_day + animals_removed.append(removed_cow) self.herd_statistics.sold_heiferIIIs_info.append( SoldAnimalTypedDict( - id=removed_heiferIII.id, - animal_type=removed_heiferIII.animal_type.value, - sold_at_day=removed_heiferIII.sold_at_day, - body_weight=removed_heiferIII.body_weight, + id=removed_cow.id, + animal_type=removed_cow.animal_type.value, + sold_at_day=removed_cow.sold_at_day, + body_weight=removed_cow.body_weight, cull_reason="NA", - days_in_milk="NA", + days_in_milk=removed_cow.days_in_milk, parity="NA", ) ) - self.herd_statistics.sold_heiferIII_oversupply_num += 1 - self.herd_statistics.heiferIII_num -= 1 + self.herd_statistics.cow_num -= 1 + self.herd_statistics.sold_cow_oversupply_num += 1 return animals_removed def _check_if_replacement_heifers_needed(self, time: RufasTime) -> list[Animal]: @@ -734,7 +737,7 @@ def _check_if_replacement_heifers_needed(self, time: RufasTime) -> list[Animal]: animals_added: list[Animal] = [] while ( self.current_herd_size + self.herd_statistics.bought_heifer_num - < self.herd_statistics.herd_num * animal_constants.BUYING_THRESHOLD + < self.herd_statistics.herd_num * animal_constants.BUYING_THRESHOLD # TODO: change to user input and time.simulation_day > 1 ): if len(self.replacement_market) == 0: diff --git a/input/data/animal/example_freestall_animal.json b/input/data/animal/example_freestall_animal.json index 36acfa20a8..b031b72c45 100644 --- a/input/data/animal/example_freestall_animal.json +++ b/input/data/animal/example_freestall_animal.json @@ -7,6 +7,9 @@ "cow_num": 100, "replace_num": 500, "herd_num": 100, + "herd_size_adjustment_period": 30, + "herd_selling_threshold": 1.03, + "herd_buying_threshold": 1.01, "breed": "HO", "parity_fractions": { "1": 0.36, 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 f439670dd0..e288eff787 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 @@ -466,7 +466,7 @@ def test_check_if_heifers_need_to_be_sold( assert result == expected_sold_heiferIIIs assert herd_manager.herd_statistics.sold_heiferIIIs_info == expected_sold_heiferIIIs_info assert herd_manager.herd_statistics.heiferIII_num == 97 - assert herd_manager.herd_statistics.sold_heiferIII_oversupply_num == 3 + assert herd_manager.herd_statistics.sold_cow_oversupply_num == 3 def test_check_if_replacement_heifers_needed( From 2b1d8e0e66e9b8b6dc9a377c603b6803543be322 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Mon, 20 Oct 2025 21:23:14 +0900 Subject: [PATCH 002/107] Updated properties --- input/metadata/properties/default.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/input/metadata/properties/default.json b/input/metadata/properties/default.json index 0443e0ed0f..74783f3c3a 100644 --- a/input/metadata/properties/default.json +++ b/input/metadata/properties/default.json @@ -83,6 +83,24 @@ "default": 100, "minimum": 6 }, + "herd_size_adjustment_period": { + "type": "number", + "description": "The period between each check to adjust the her's size.", + "default": 30, + "minimum": 1 + }, + "herd_selling_threshold": { + "type": "number", + "description": "The threshold that should sell animals to maintain herd size.", + "default": 1.03, + "minimum": 1 + }, + "herd_buying_threshold": { + "type": "number", + "description": "The threshold that should buy animals to maintain herd size.", + "default": 1.01, + "minimum": 1 + }, "breed": { "type": "string", "default": "HO", From d75e3b1d45eb896bc83d690a30f48694862cb43a Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Sat, 25 Oct 2025 02:21:26 +0900 Subject: [PATCH 003/107] Working implementation --- RUFAS/biophysical/animal/animal.py | 8 --- RUFAS/biophysical/animal/herd_manager.py | 70 ++++++++++++------- .../test_herd_manager_daily_routines.py | 2 +- 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/RUFAS/biophysical/animal/animal.py b/RUFAS/biophysical/animal/animal.py index 12f8618c0d..785357385a 100644 --- a/RUFAS/biophysical/animal/animal.py +++ b/RUFAS/biophysical/animal/animal.py @@ -1748,14 +1748,6 @@ def animal_life_stage_update(self, time: RufasTime) -> tuple[AnimalStatus, NewBo self.cull_reason = animal_constants.DEATH_CULL animal_status = AnimalStatus.DEAD - if ( - self.animal_type.is_cow - and self.reproduction.do_not_breed - and self.milk_production.daily_milk_produced < AnimalConfig.cull_milk_production - ): - self.cull_reason = animal_constants.LOW_PROD_CULL - self.sold_at_day = time.simulation_day - animal_status = AnimalStatus.SOLD return animal_status, newborn_calf_config def _evaluate_calf_for_heiferI(self) -> bool: diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 00d0005f46..ad919d3160 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -127,6 +127,8 @@ def __init__( self.herd_statistics = HerdStatistics() self.herd_statistics.herd_num = animal_config_data["herd_information"]["herd_num"] self.adjustment_period = animal_config_data["herd_information"]["herd_size_adjustment_period"] + self.selling_threshold = animal_config_data["herd_information"]["herd_selling_threshold"] + self.buying_threshold = animal_config_data["herd_information"]["herd_buying_threshold"] self.herd_reproduction_statistics = HerdReproductionStatistics() self.housing = animal_config_data["housing"] @@ -590,8 +592,8 @@ def daily_routines( self._update_stillborn_calf_statistics(stillborn_newborn_calves) - if time.simulation_day > 0 and time.simulation_day % 60 == 0: - removed_animals += self._check_if_heifers_need_to_be_sold(simulation_day=time.simulation_day) + if time.simulation_day > 0 and time.simulation_day % self.adjustment_period == 0: + removed_animals += self._check_if_cows_need_to_be_sold(simulation_day=time.simulation_day) newly_added_animals = self._check_if_replacement_heifers_needed(time=time) self._update_herd_structure( graduated_animals=graduated_animals, @@ -668,39 +670,56 @@ def _create_newborn_calf(self, newborn_calf_config: NewBornCalfValuesTypedDict, newborn_calf.events.add_event(newborn_calf.days_born, simulation_day, animal_constants.ENTER_HERD) return newborn_calf - def _check_if_heifers_need_to_be_sold( + def _check_if_cows_need_to_be_sold( self, simulation_day: int, ) -> list[Animal]: """ - Checks if surplus heifers need to be sold based on herd size. - - This method evaluates if the current number of heifers and cows exceeds a - specified threshold (defined as 3% over the herd statistics' target - herd size). If the threshold is surpassed, heiferIIIs are removed from the - herd until the herd size falls within the acceptable range. - - Parameters - ---------- - simulation_day : int - The simulation day on which the check and potential sale is conducted. - - Returns - ------- - list[Animal] - A list of heiferIIIs to be sold. + Checks if surplus cows need to be sold based on herd size. + Rule: + - Prefer removing cows with reproduction.do_not_breed == True, ordered by lowest daily milk. + - If none, remove non-DNB cows ordered by lowest 305-day milk. """ animals_removed: list[Animal] = [] + while ( - self.current_herd_size > self.herd_statistics.herd_num * animal_constants.SELLING_THRESHOLD # TODO: Change to user input - and len(self.heiferIIIs) > 0 + self.current_herd_size > self.herd_statistics.herd_num * self.selling_threshold + and len(self.cows) > 0 ): - self.cows.sort(key=lambda cow: cow.milk_production.daily_milk_produced) - removed_cow = self.cows.pop() + # Partitioning between dnb and non dnb cows + dnb_indices: list[int] = [] + non_dnb_indices: list[int] = [] + for i, c in enumerate(self.cows): + if c.reproduction.do_not_breed: + dnb_indices.append(i) + else: + non_dnb_indices.append(i) + + if dnb_indices: # when there is dnb to remove + lowest_production_cow_index = 0 + current_lowest_production_value = math.inf + for i in dnb_indices: + daily_milk_produced = self.cows[i].milk_production.daily_milk_produced + if daily_milk_produced < current_lowest_production_value: + current_lowest_production_value = daily_milk_produced + lowest_production_cow_index = i + remove_index = lowest_production_cow_index + else: + lowest_production_cow_index = 0 + current_lowest_estimation_value = math.inf + for i in non_dnb_indices: + estimated_production = self.cows[i].milk_production.current_lactation_305_day_milk_produced + if estimated_production < current_lowest_estimation_value: + current_lowest_estimation_value = estimated_production + lowest_production_cow_index = i + remove_index = lowest_production_cow_index + + removed_cow = self.cows.pop(remove_index) removed_cow.sold_at_day = simulation_day animals_removed.append(removed_cow) - self.herd_statistics.sold_heiferIIIs_info.append( + + self.herd_statistics.sold_cows_info.append( SoldAnimalTypedDict( id=removed_cow.id, animal_type=removed_cow.animal_type.value, @@ -713,6 +732,7 @@ def _check_if_heifers_need_to_be_sold( ) self.herd_statistics.cow_num -= 1 self.herd_statistics.sold_cow_oversupply_num += 1 + return animals_removed def _check_if_replacement_heifers_needed(self, time: RufasTime) -> list[Animal]: @@ -737,7 +757,7 @@ def _check_if_replacement_heifers_needed(self, time: RufasTime) -> list[Animal]: animals_added: list[Animal] = [] while ( self.current_herd_size + self.herd_statistics.bought_heifer_num - < self.herd_statistics.herd_num * animal_constants.BUYING_THRESHOLD # TODO: change to user input + < self.herd_statistics.herd_num * self.buying_threshold and time.simulation_day > 1 ): if len(self.replacement_market) == 0: 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 e288eff787..d5ebe76042 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 @@ -448,7 +448,7 @@ def test_check_if_heifers_need_to_be_sold( len(herd_manager.cows), ) - result = herd_manager._check_if_heifers_need_to_be_sold(simulation_day=0) + result = herd_manager._check_if_cows_need_to_be_sold(simulation_day=0) expected_sold_heiferIIIs = mock_herd["heiferIIIs"][::-1][:3] expected_sold_heiferIIIs_info = [ From ddd13c510ed887d9030b4517f29e511a4c20c54e Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Wed, 29 Oct 2025 03:13:47 +0900 Subject: [PATCH 004/107] Changes to track lactation predictions --- RUFAS/biophysical/animal/animal.py | 1 + RUFAS/biophysical/animal/herd_manager.py | 12 ++++++++++++ RUFAS/biophysical/animal/milk/milk_production.py | 11 ++++++++++- RUFAS/simulation_engine.py | 1 + 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/RUFAS/biophysical/animal/animal.py b/RUFAS/biophysical/animal/animal.py index 785357385a..64ec8cc077 100644 --- a/RUFAS/biophysical/animal/animal.py +++ b/RUFAS/biophysical/animal/animal.py @@ -201,6 +201,7 @@ def __init__( self.nutrition_supply: NutritionSupply = NutritionSupply.make_empty_nutrition_supply() self.nutrition_supply.dry_matter = AnimalModuleConstants.DEFAULT_DRY_MATTER_INTAKE self.previous_nutrition_supply: NutritionSupply | None = None + self.milk_yield_305_day: float = 0.0 self._days_in_milk: int = 0 self._milk_production_output_days_in_milk: int = 0 diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index ad919d3160..fd45af9963 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -2040,3 +2040,15 @@ def _update_total_enteric_methane(self, digestive_outputs: list[dict[AnimalType, self.herd_statistics.total_enteric_methane[animal_type] = { k: float(current_totals.get(k, 0) + new_emissions.get(k, 0)) for k in all_keys } + + def update_milk_305_day_yield_predictions(self) -> None: + for cow in self.cows: + if cow.days_in_milk == 0: + cow.milk_yield_305_day = cow.milk_production.calc_305_day_milk_yield(cow.milk_production.wood_l, + cow.milk_production.wood_m, + cow.milk_production.wood_n) + elif 1 <= cow.days_in_milk < 306: + cow.milk_yield_305_day = cow.milk_production.calculate_mature_equivalent_305_day_milk_yield( + cow.calves, cow.days_in_milk) + else: + cow.milk_yield_305_day = cow.milk_production.current_lactation_305_day_milk_produced diff --git a/RUFAS/biophysical/animal/milk/milk_production.py b/RUFAS/biophysical/animal/milk/milk_production.py index 9adebbbb25..48d9ccb9f2 100644 --- a/RUFAS/biophysical/animal/milk/milk_production.py +++ b/RUFAS/biophysical/animal/milk/milk_production.py @@ -283,10 +283,19 @@ def calc_305_day_milk_yield(l_param: float, m_param: float, n_param: float) -> f 305 day milk yield for a cow with the given lactation curve (kg). """ - result, _ = quad(MilkProduction.calculate_daily_milk_production, 1, 305, args=(l_param, m_param, n_param)) return result + def calculate_mature_equivalent_305_day_milk_yield(self, parity: int, days_in_milk: int) -> float: + result, _ = quad(MilkProduction.calculate_daily_milk_production, days_in_milk + 1, 305, + args=(self.wood_l,self.wood_m, self.wood_n)) + if parity == 1: + return sum(self.milk_production_history[:days_in_milk]) + result # TODO: adjust these numbers regarding adjustment factor? + elif parity == 2: + return sum(self.milk_production_history[:days_in_milk]) + result + else: + return sum(self.milk_production_history[:days_in_milk]) + result + def _get_milk_production_adjustment(self) -> float: """ Randomly adjusts the milk production on a specific day. diff --git a/RUFAS/simulation_engine.py b/RUFAS/simulation_engine.py index 659fe5f8c1..28d1da7dc9 100644 --- a/RUFAS/simulation_engine.py +++ b/RUFAS/simulation_engine.py @@ -127,6 +127,7 @@ def _daily_simulation(self) -> None: is_time_to_reformulate_ration = self.time.current_date.date() == self.next_ration_reformulation if is_time_to_reformulate_ration: self._formulate_ration() + self.herd_manager.update_milk_305_day_yield_predictions() requested_feed = self.herd_manager.collect_daily_feed_request() self.feed_manager.report_feed_storage_levels(self.time.simulation_day, "daily_storage_levels") From a0bb502a000dc692d9f4fdb3edb0e56572763164 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Tue, 4 Nov 2025 13:42:36 +0900 Subject: [PATCH 005/107] mvp --- RUFAS/biophysical/animal/animal.py | 16 +++++++ RUFAS/biophysical/animal/herd_manager.py | 42 ++++++++++--------- .../animal/milk/milk_production.py | 9 +--- 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/RUFAS/biophysical/animal/animal.py b/RUFAS/biophysical/animal/animal.py index 64ec8cc077..49d0d9956f 100644 --- a/RUFAS/biophysical/animal/animal.py +++ b/RUFAS/biophysical/animal/animal.py @@ -202,6 +202,7 @@ def __init__( self.nutrition_supply.dry_matter = AnimalModuleConstants.DEFAULT_DRY_MATTER_INTAKE self.previous_nutrition_supply: NutritionSupply | None = None self.milk_yield_305_day: float = 0.0 + self.milk_prediction_305_day: float = 0.0 self._days_in_milk: int = 0 self._milk_production_output_days_in_milk: int = 0 @@ -2360,3 +2361,18 @@ def calculate_nutrition_requirements( ) return requirements + + def update_305_days_milk_production(self) -> None: + if self.days_in_milk == 0: + self.milk_prediction_305_day = self.milk_production.calc_305_day_milk_yield(self.milk_production.wood_l, + self.milk_production.wood_m, + self.milk_production.wood_n) + elif 1 <= self.days_in_milk < 305: + self.milk_prediction_305_day = self.milk_production.calculate_mature_equivalent_305_day_milk_yield( + self.days_in_milk) + else: + self.milk_prediction_305_day = self.milk_production.current_lactation_305_day_milk_produced + + parity_factor = {1: 1.25, 2: 1.18}.get(self.calves, 1.0) + + self.milk_prediction_305_day = self.milk_prediction_305_day * parity_factor diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index fd45af9963..d6662d01cd 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -681,38 +681,48 @@ def _check_if_cows_need_to_be_sold( - Prefer removing cows with reproduction.do_not_breed == True, ordered by lowest daily milk. - If none, remove non-DNB cows ordered by lowest 305-day milk. """ + MIN_DIM_FOR_REMOVAL = 60 animals_removed: list[Animal] = [] while ( self.current_herd_size > self.herd_statistics.herd_num * self.selling_threshold and len(self.cows) > 0 ): - # Partitioning between dnb and non dnb cows + # partitioning between dnb and non dnb cows dnb_indices: list[int] = [] non_dnb_indices: list[int] = [] - for i, c in enumerate(self.cows): - if c.reproduction.do_not_breed: - dnb_indices.append(i) + for index, cow in enumerate(self.cows): + if cow.days_in_milk < MIN_DIM_FOR_REMOVAL: + continue + if cow.reproduction.do_not_breed: + dnb_indices.append(index) else: - non_dnb_indices.append(i) + non_dnb_indices.append(index) + + if not dnb_indices and not non_dnb_indices: + self.om.add_error("Unable to adjust herd size", + "There are no cow that's qualified to be sold.", + {} + ) + break if dnb_indices: # when there is dnb to remove lowest_production_cow_index = 0 current_lowest_production_value = math.inf - for i in dnb_indices: - daily_milk_produced = self.cows[i].milk_production.daily_milk_produced + for index in dnb_indices: + daily_milk_produced = self.cows[index].milk_production.daily_milk_produced if daily_milk_produced < current_lowest_production_value: current_lowest_production_value = daily_milk_produced - lowest_production_cow_index = i + lowest_production_cow_index = index remove_index = lowest_production_cow_index else: lowest_production_cow_index = 0 current_lowest_estimation_value = math.inf - for i in non_dnb_indices: - estimated_production = self.cows[i].milk_production.current_lactation_305_day_milk_produced + for index in non_dnb_indices: + estimated_production = self.cows[index].milk_prediction_305_day if estimated_production < current_lowest_estimation_value: current_lowest_estimation_value = estimated_production - lowest_production_cow_index = i + lowest_production_cow_index = index remove_index = lowest_production_cow_index removed_cow = self.cows.pop(remove_index) @@ -2043,12 +2053,4 @@ def _update_total_enteric_methane(self, digestive_outputs: list[dict[AnimalType, def update_milk_305_day_yield_predictions(self) -> None: for cow in self.cows: - if cow.days_in_milk == 0: - cow.milk_yield_305_day = cow.milk_production.calc_305_day_milk_yield(cow.milk_production.wood_l, - cow.milk_production.wood_m, - cow.milk_production.wood_n) - elif 1 <= cow.days_in_milk < 306: - cow.milk_yield_305_day = cow.milk_production.calculate_mature_equivalent_305_day_milk_yield( - cow.calves, cow.days_in_milk) - else: - cow.milk_yield_305_day = cow.milk_production.current_lactation_305_day_milk_produced + cow.update_305_days_milk_production() diff --git a/RUFAS/biophysical/animal/milk/milk_production.py b/RUFAS/biophysical/animal/milk/milk_production.py index 48d9ccb9f2..74b403d341 100644 --- a/RUFAS/biophysical/animal/milk/milk_production.py +++ b/RUFAS/biophysical/animal/milk/milk_production.py @@ -286,15 +286,10 @@ def calc_305_day_milk_yield(l_param: float, m_param: float, n_param: float) -> f result, _ = quad(MilkProduction.calculate_daily_milk_production, 1, 305, args=(l_param, m_param, n_param)) return result - def calculate_mature_equivalent_305_day_milk_yield(self, parity: int, days_in_milk: int) -> float: + def calculate_mature_equivalent_305_day_milk_yield(self, days_in_milk: int) -> float: result, _ = quad(MilkProduction.calculate_daily_milk_production, days_in_milk + 1, 305, args=(self.wood_l,self.wood_m, self.wood_n)) - if parity == 1: - return sum(self.milk_production_history[:days_in_milk]) + result # TODO: adjust these numbers regarding adjustment factor? - elif parity == 2: - return sum(self.milk_production_history[:days_in_milk]) + result - else: - return sum(self.milk_production_history[:days_in_milk]) + result + return sum(history["milk_production"] for history in self.milk_production_history[:days_in_milk]) + result def _get_milk_production_adjustment(self) -> float: """ From ac0a87b90b122e3517d40b84cf9942023a0f4356 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Tue, 4 Nov 2025 14:20:49 +0900 Subject: [PATCH 006/107] MVP with correct reporting --- RUFAS/biophysical/animal/animal_module_reporter.py | 2 +- RUFAS/biophysical/animal/data_types/herd_statistics.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RUFAS/biophysical/animal/animal_module_reporter.py b/RUFAS/biophysical/animal/animal_module_reporter.py index 5c1c285f84..2aa9c3c021 100644 --- a/RUFAS/biophysical/animal/animal_module_reporter.py +++ b/RUFAS/biophysical/animal/animal_module_reporter.py @@ -589,7 +589,7 @@ def report_herd_statistics_data(cls, herd_statistics: HerdStatistics, simulation "data_origin": [("HerdManager", "daily_update")], } om.add_variable( - "sold_heiferIII_oversupply_num", + "sold_cow_oversupply_num", herd_statistics.sold_cow_oversupply_num, dict(info_map, **{"units": MeasurementUnits.ANIMALS}), ) diff --git a/RUFAS/biophysical/animal/data_types/herd_statistics.py b/RUFAS/biophysical/animal/data_types/herd_statistics.py index 562064a855..ca49b5fcef 100644 --- a/RUFAS/biophysical/animal/data_types/herd_statistics.py +++ b/RUFAS/biophysical/animal/data_types/herd_statistics.py @@ -48,7 +48,7 @@ class HerdStatistics: sold_calf_num : int Number of calves sold during a specific period, (unitless). sold_cow_oversupply_num : int - Number of surplus "Heifer III" animals sold, (unitless). + Number of surplus cow sold, (unitless). bought_heifer_num : int Number of heifers purchased during a specific period, (unitless). sold_heiferII_num : int From 86e4d6776b0470510c3a9aa62f81a1a4a19d1439 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Tue, 4 Nov 2025 14:33:34 +0900 Subject: [PATCH 007/107] Updated open lot inputs --- input/data/animal/example_open_lot_animal.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/input/data/animal/example_open_lot_animal.json b/input/data/animal/example_open_lot_animal.json index 198135a902..c31f2b8a0a 100644 --- a/input/data/animal/example_open_lot_animal.json +++ b/input/data/animal/example_open_lot_animal.json @@ -7,6 +7,9 @@ "cow_num": 1000, "replace_num": 5000, "herd_num": 1000, + "herd_size_adjustment_period": 30, + "herd_selling_threshold": 1.03, + "herd_buying_threshold": 1.01, "breed": "HO", "parity_fractions": { "1": 0.35, From f063d83a43bbc422311cf5377c655d3159333121 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Tue, 4 Nov 2025 14:41:22 +0900 Subject: [PATCH 008/107] Updated changelog.md --- changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 6a7a66907d..245a1976d6 100644 --- a/changelog.md +++ b/changelog.md @@ -261,7 +261,7 @@ v0.9.2 - [2618](https://github.com/RuminantFarmSystems/MASM/pull/2618) - [minor change] Removes outdated example regional feed inputs. - [2624](https://github.com/RuminantFarmSystems/MASM/pull/2624) - [minor change] [NoInputChange][NoOutputChange] Updates the PR template and changelog to reflect IO changes. - [1299](https://github.com/RuminantFarmSystems/MASM/pull/1299) - [minor change] [InputChange][OutputChange] Implements the Energy submodule of the EEE module with diesel consumption calculation. - +- [2622](https://github.com/RuminantFarmSystems/MASM/pull/2622) - [minor change] [InputChange][OutputChange] Implement 305 days milk production logic and utilizing such merit to maintain herd size. ### v0.9.2 From 16e954b3d0b4d617101abef63fb4640f408387c1 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Thu, 6 Nov 2025 00:11:07 +0900 Subject: [PATCH 009/107] Cleared some mypy and updated original tests --- RUFAS/biophysical/animal/herd_manager.py | 2 +- .../test_animal/test_animal/test_animal.py | 50 ------------------- .../test_herd_manager/pytest_fixtures.py | 3 ++ .../test_herd_manager_daily_routines.py | 21 +++----- 4 files changed, 10 insertions(+), 66 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index d6662d01cd..71b99e1cf0 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -706,7 +706,7 @@ def _check_if_cows_need_to_be_sold( ) break - if dnb_indices: # when there is dnb to remove + if dnb_indices: # when there is dnb to remove lowest_production_cow_index = 0 current_lowest_production_value = math.inf for index in dnb_indices: 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 b96c1eace8..4491c4d868 100644 --- a/tests/test_biophysical/test_animal/test_animal/test_animal.py +++ b/tests/test_biophysical/test_animal/test_animal/test_animal.py @@ -2451,56 +2451,6 @@ def test_animal_life_stage_update_not_cow( ) -@pytest.mark.parametrize("future_cull_date,future_death_date,expected_status", [(15, 15, AnimalStatus.SOLD)]) -def test_animal_life_stage_update_low_production( - mock_lactating_cow: Animal, - mocker: MockerFixture, - future_cull_date: int, - future_death_date: int, - expected_status: AnimalStatus, -) -> None: - mock_lactating_cow.animal_type = AnimalType.LAC_COW - mock_lactating_cow.future_cull_date = future_cull_date - mock_lactating_cow.future_death_date = future_death_date - mock_lactating_cow.reproduction.do_not_breed = True - mock_lactating_cow.milk_production.daily_milk_produced = 5 - mocker.patch.object(RufasTime, "simulation_day", new_callable=PropertyMock, return_value=5) - time = RufasTime(datetime(year=1999, month=1, day=2), datetime(year=2000, month=1, day=1)) - mock_update = mocker.patch.object( - mock_lactating_cow, - "_cow_life_stage_update", - return_value=( - AnimalStatus.LIFE_STAGE_CHANGED, - NewBornCalfValuesTypedDict( - breed="test_breed", - animal_type="test_type", - birth_date="test_bd", - days_born=5, - birth_weight=15.3, - initial_phosphorus=18.4, - net_merit=75.1, - ), - ), - ) - - status, output = mock_lactating_cow.animal_life_stage_update(time) - - mock_update.assert_called_once() - - assert mock_lactating_cow.cull_reason == animal_constants.LOW_PROD_CULL - assert mock_lactating_cow.sold_at_day == 5 - assert status == AnimalStatus.SOLD - assert output == NewBornCalfValuesTypedDict( - breed="test_breed", - animal_type="test_type", - birth_date="test_bd", - days_born=5, - birth_weight=15.3, - initial_phosphorus=18.4, - net_merit=75.1, - ) - - @pytest.mark.parametrize("born_days, expected", [(10, False), (60, True)]) def test_evaluate_calf_for_heiferI(mock_lactating_cow: Animal, born_days: int, expected: bool) -> None: mock_lactating_cow.days_born = born_days 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 732a0c087a..0ee108ef8f 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 @@ -43,6 +43,9 @@ def config_json() -> dict[str, Any]: def animal_json() -> dict[str, Any]: return { "herd_information": { + "herd_size_adjustment_period": 30, + "herd_selling_threshold": 1.03, + "herd_buying_threshold": 1.01, "calf_num": 8, "heiferI_num": 44, "heiferII_num": 38, 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 d5ebe76042..36dacc7e1d 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 @@ -329,8 +329,8 @@ def test_daily_routines(herd_manager: HerdManager, mock_herd: dict[str, list[Ani side_effect=mock_perform_daily_routines_for_animals_side_effect, ) mock_update_sold_animal_statistics = mocker.patch.object(herd_manager, "_update_sold_animal_statistics") - mock_check_if_heifers_need_to_be_sold = mocker.patch.object( - herd_manager, "_check_if_heifers_need_to_be_sold", return_value=sold_oversupply_heiferIIIs + mock_check_if_cows_need_to_be_sold = mocker.patch.object( + herd_manager, "_check_if_cows_need_to_be_sold", return_value=sold_oversupply_heiferIIIs ) mock_check_if_replacement_heifers_needed = mocker.patch.object( herd_manager, "_check_if_replacement_heifers_needed", return_value=bought_replacement_heiferIIIs @@ -375,18 +375,9 @@ 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 ) - mock_check_if_heifers_need_to_be_sold.assert_called_once_with(simulation_day=mock_time.simulation_day) - mock_check_if_replacement_heifers_needed.assert_called_once_with(time=mock_time) - mock_update_herd_structure.assert_called_once_with( - graduated_animals=graduated_animals, - newborn_calves=newborn_calves, - newly_added_animals=bought_replacement_heiferIIIs, - removed_animals=removed_animals, - available_feeds=[mock_feed], - current_day_conditions=mock_weather.get_current_day_conditions(), - total_inventory=mock_total_inventory, - simulation_day=15, - ) + assert mock_check_if_cows_need_to_be_sold.call_count == 0 + assert mock_check_if_replacement_heifers_needed.call_count == 0 + assert mock_update_herd_structure.call_count == 0 mock_record_pen_history.assert_called_once_with(mock_time.simulation_day) mock_update_herd_statistics.assert_called_once_with() mock_report_manure_streams.assert_called_once() @@ -429,7 +420,7 @@ def test_create_newborn_calf( animal.events.add_event.assert_called_once() -def test_check_if_heifers_need_to_be_sold( +def test_check_if_cows_need_to_be_sold( mock_get_data_side_effect: list[Any], mocker: MockerFixture, mock_herd: dict[str, list[Animal]] ) -> None: """Unit test for _check_if_heifers_need_to_be_sold()""" From 8aca097cfa35b4c4d0d4d4be82f74748516b8673 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Thu, 6 Nov 2025 00:13:21 +0900 Subject: [PATCH 010/107] Fix for code factor error --- RUFAS/biophysical/animal/animal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RUFAS/biophysical/animal/animal.py b/RUFAS/biophysical/animal/animal.py index 49d0d9956f..f4e2901fbe 100644 --- a/RUFAS/biophysical/animal/animal.py +++ b/RUFAS/biophysical/animal/animal.py @@ -2375,4 +2375,4 @@ def update_305_days_milk_production(self) -> None: parity_factor = {1: 1.25, 2: 1.18}.get(self.calves, 1.0) - self.milk_prediction_305_day = self.milk_prediction_305_day * parity_factor + self.milk_prediction_305_day = self.milk_prediction_305_day * parity_factor From 3706b26020f7d0f8f77f9be8f1f567918d0d2b83 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Mon, 10 Nov 2025 17:40:00 +0900 Subject: [PATCH 011/107] Combined predictions and improved naming --- RUFAS/biophysical/animal/animal.py | 20 +++++++++--------- RUFAS/biophysical/animal/herd_manager.py | 4 ++-- .../animal/milk/lactation_curve.py | 2 +- .../animal/milk/milk_production.py | 21 ++++++++++++------- .../test_milk_production.py | 2 +- 5 files changed, 28 insertions(+), 21 deletions(-) diff --git a/RUFAS/biophysical/animal/animal.py b/RUFAS/biophysical/animal/animal.py index f4e2901fbe..142d401bc9 100644 --- a/RUFAS/biophysical/animal/animal.py +++ b/RUFAS/biophysical/animal/animal.py @@ -202,7 +202,7 @@ def __init__( self.nutrition_supply.dry_matter = AnimalModuleConstants.DEFAULT_DRY_MATTER_INTAKE self.previous_nutrition_supply: NutritionSupply | None = None self.milk_yield_305_day: float = 0.0 - self.milk_prediction_305_day: float = 0.0 + self.mature_equivalent_milking_prediction_305_day: float = 0.0 self._days_in_milk: int = 0 self._milk_production_output_days_in_milk: int = 0 @@ -2362,17 +2362,17 @@ def calculate_nutrition_requirements( return requirements - def update_305_days_milk_production(self) -> None: - if self.days_in_milk == 0: - self.milk_prediction_305_day = self.milk_production.calc_305_day_milk_yield(self.milk_production.wood_l, - self.milk_production.wood_m, - self.milk_production.wood_n) - elif 1 <= self.days_in_milk < 305: - self.milk_prediction_305_day = self.milk_production.calculate_mature_equivalent_305_day_milk_yield( + def update_mature_equivalent_305_days_milk_production(self) -> None: + if self.days_in_milk < 305: + self.mature_equivalent_milking_prediction_305_day = self.milk_production.calculate_305_day_milk_yield( + self.milk_production.wood_l, + self.milk_production.wood_m, + self.milk_production.wood_n, + self.milk_production.milk_production_history, self.days_in_milk) else: - self.milk_prediction_305_day = self.milk_production.current_lactation_305_day_milk_produced + self.mature_equivalent_milking_prediction_305_day = self.milk_production.current_lactation_305_day_milk_produced parity_factor = {1: 1.25, 2: 1.18}.get(self.calves, 1.0) - self.milk_prediction_305_day = self.milk_prediction_305_day * parity_factor + self.mature_equivalent_milking_prediction_305_day = self.mature_equivalent_milking_prediction_305_day * parity_factor diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 71b99e1cf0..a868f7bae4 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -719,7 +719,7 @@ def _check_if_cows_need_to_be_sold( lowest_production_cow_index = 0 current_lowest_estimation_value = math.inf for index in non_dnb_indices: - estimated_production = self.cows[index].milk_prediction_305_day + estimated_production = self.cows[index].mature_equivalent_milking_prediction_305_day if estimated_production < current_lowest_estimation_value: current_lowest_estimation_value = estimated_production lowest_production_cow_index = index @@ -2053,4 +2053,4 @@ def _update_total_enteric_methane(self, digestive_outputs: list[dict[AnimalType, def update_milk_305_day_yield_predictions(self) -> None: for cow in self.cows: - cow.update_305_days_milk_production() + cow.update_mature_equivalent_305_days_milk_production() diff --git a/RUFAS/biophysical/animal/milk/lactation_curve.py b/RUFAS/biophysical/animal/milk/lactation_curve.py index 204c459d25..343f8791d8 100644 --- a/RUFAS/biophysical/animal/milk/lactation_curve.py +++ b/RUFAS/biophysical/animal/milk/lactation_curve.py @@ -377,7 +377,7 @@ def _estimate_305_day_milk_yield_by_parity( @staticmethod def _calculate_305_day_milk_yield_error(l_param: float, m_param: float, n_param: float, milk_yield: float) -> float: """Calculates absolute difference between an estimated 305 day milk yield and a predetermined one.""" - return abs(MilkProduction.calc_305_day_milk_yield(l_param, m_param, n_param) - milk_yield) + return abs(MilkProduction.calculate_305_day_milk_yield(l_param, m_param, n_param, None) - milk_yield) @classmethod def _fit_wood_l_param_to_milk_yield( diff --git a/RUFAS/biophysical/animal/milk/milk_production.py b/RUFAS/biophysical/animal/milk/milk_production.py index 74b403d341..500f177688 100644 --- a/RUFAS/biophysical/animal/milk/milk_production.py +++ b/RUFAS/biophysical/animal/milk/milk_production.py @@ -260,7 +260,9 @@ def calculate_daily_milk_production(days_in_milk: int, l_param: float, m_param: return l_param * np.power(days_in_milk, m_param) * np.exp(-1 * n_param * days_in_milk) @staticmethod - def calc_305_day_milk_yield(l_param: float, m_param: float, n_param: float) -> float: + def calculate_305_day_milk_yield(l_param: float, m_param: float, n_param: float, + milking_history: list[MilkProductionRecord] | None, + days_in_milk: int = 0) -> float: """ Calculates the total milk yield from day 1 to day 305 of the lactation. @@ -276,6 +278,10 @@ def calc_305_day_milk_yield(l_param: float, m_param: float, n_param: float) -> f Wood's lactation curve parameter m. n_param: float Wood's lactation curve parameter n. + days_in_milk : int + Days in milk. + milking_history : + The milk production history if the animal. Returns ------- @@ -283,13 +289,14 @@ def calc_305_day_milk_yield(l_param: float, m_param: float, n_param: float) -> f 305 day milk yield for a cow with the given lactation curve (kg). """ - result, _ = quad(MilkProduction.calculate_daily_milk_production, 1, 305, args=(l_param, m_param, n_param)) - return result + production_history_sum = 0 + if 305 > days_in_milk > 0 and milking_history is not None: + production_history_sum = sum(history["milk_production"] for history in milking_history[:days_in_milk]) + + result, _ = quad( + MilkProduction.calculate_daily_milk_production, days_in_milk + 1, 305, args=(l_param, m_param, n_param)) - def calculate_mature_equivalent_305_day_milk_yield(self, days_in_milk: int) -> float: - result, _ = quad(MilkProduction.calculate_daily_milk_production, days_in_milk + 1, 305, - args=(self.wood_l,self.wood_m, self.wood_n)) - return sum(history["milk_production"] for history in self.milk_production_history[:days_in_milk]) + result + return result + production_history_sum def _get_milk_production_adjustment(self) -> float: """ diff --git a/tests/test_biophysical/test_animal/test_milk_production/test_milk_production.py b/tests/test_biophysical/test_animal/test_milk_production/test_milk_production.py index ab4f55f4e7..f3a72607d0 100644 --- a/tests/test_biophysical/test_animal/test_milk_production/test_milk_production.py +++ b/tests/test_biophysical/test_animal/test_milk_production/test_milk_production.py @@ -202,7 +202,7 @@ def test_calculate_daily_milk_production( ) def test_calc_305_day_milk_yield(l_param: float, m_param: float, n_param: float, expected: float) -> None: """Test the calc_305_day_milk_yield method of the MilkProduction class.""" - assert MilkProduction.calc_305_day_milk_yield(l_param, m_param, n_param) == pytest.approx(expected, rel=1e-6) + assert MilkProduction.calculate_305_day_milk_yield(l_param, m_param, n_param, None) == pytest.approx(expected, rel=1e-6) @pytest.mark.parametrize( From 26e610ab2c3909e180f60608d78dcc24d1ca3b5b Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Mon, 10 Nov 2025 17:41:12 +0900 Subject: [PATCH 012/107] codefactor --- RUFAS/biophysical/animal/milk/milk_production.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RUFAS/biophysical/animal/milk/milk_production.py b/RUFAS/biophysical/animal/milk/milk_production.py index 500f177688..9c4f907314 100644 --- a/RUFAS/biophysical/animal/milk/milk_production.py +++ b/RUFAS/biophysical/animal/milk/milk_production.py @@ -293,7 +293,7 @@ def calculate_305_day_milk_yield(l_param: float, m_param: float, n_param: float, if 305 > days_in_milk > 0 and milking_history is not None: production_history_sum = sum(history["milk_production"] for history in milking_history[:days_in_milk]) - result, _ = quad( + result, _ = quad( MilkProduction.calculate_daily_milk_production, days_in_milk + 1, 305, args=(l_param, m_param, n_param)) return result + production_history_sum From 60d41a27e25d7b01d840f2ff1bf7f293aefb835a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 25 Nov 2025 14:16:53 +0000 Subject: [PATCH 013/107] Apply Black Formatting --- RUFAS/biophysical/animal/animal.py | 11 ++++++++--- RUFAS/biophysical/animal/herd_manager.py | 10 ++-------- RUFAS/biophysical/animal/milk/milk_production.py | 13 +++++++++---- .../test_milk_production/test_milk_production.py | 4 +++- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/RUFAS/biophysical/animal/animal.py b/RUFAS/biophysical/animal/animal.py index 142d401bc9..14f49b0fdb 100644 --- a/RUFAS/biophysical/animal/animal.py +++ b/RUFAS/biophysical/animal/animal.py @@ -2369,10 +2369,15 @@ def update_mature_equivalent_305_days_milk_production(self) -> None: self.milk_production.wood_m, self.milk_production.wood_n, self.milk_production.milk_production_history, - self.days_in_milk) + self.days_in_milk, + ) else: - self.mature_equivalent_milking_prediction_305_day = self.milk_production.current_lactation_305_day_milk_produced + self.mature_equivalent_milking_prediction_305_day = ( + self.milk_production.current_lactation_305_day_milk_produced + ) parity_factor = {1: 1.25, 2: 1.18}.get(self.calves, 1.0) - self.mature_equivalent_milking_prediction_305_day = self.mature_equivalent_milking_prediction_305_day * parity_factor + self.mature_equivalent_milking_prediction_305_day = ( + self.mature_equivalent_milking_prediction_305_day * parity_factor + ) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 2c34fa42d1..864f766b2f 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -688,10 +688,7 @@ def _check_if_cows_need_to_be_sold( MIN_DIM_FOR_REMOVAL = 60 animals_removed: list[Animal] = [] - while ( - self.current_herd_size > self.herd_statistics.herd_num * self.selling_threshold - and len(self.cows) > 0 - ): + while self.current_herd_size > self.herd_statistics.herd_num * self.selling_threshold and len(self.cows) > 0: # partitioning between dnb and non dnb cows dnb_indices: list[int] = [] non_dnb_indices: list[int] = [] @@ -704,10 +701,7 @@ def _check_if_cows_need_to_be_sold( non_dnb_indices.append(index) if not dnb_indices and not non_dnb_indices: - self.om.add_error("Unable to adjust herd size", - "There are no cow that's qualified to be sold.", - {} - ) + self.om.add_error("Unable to adjust herd size", "There are no cow that's qualified to be sold.", {}) break if dnb_indices: # when there is dnb to remove diff --git a/RUFAS/biophysical/animal/milk/milk_production.py b/RUFAS/biophysical/animal/milk/milk_production.py index 9c4f907314..fc9ecf17a0 100644 --- a/RUFAS/biophysical/animal/milk/milk_production.py +++ b/RUFAS/biophysical/animal/milk/milk_production.py @@ -260,9 +260,13 @@ def calculate_daily_milk_production(days_in_milk: int, l_param: float, m_param: return l_param * np.power(days_in_milk, m_param) * np.exp(-1 * n_param * days_in_milk) @staticmethod - def calculate_305_day_milk_yield(l_param: float, m_param: float, n_param: float, - milking_history: list[MilkProductionRecord] | None, - days_in_milk: int = 0) -> float: + def calculate_305_day_milk_yield( + l_param: float, + m_param: float, + n_param: float, + milking_history: list[MilkProductionRecord] | None, + days_in_milk: int = 0, + ) -> float: """ Calculates the total milk yield from day 1 to day 305 of the lactation. @@ -294,7 +298,8 @@ def calculate_305_day_milk_yield(l_param: float, m_param: float, n_param: float, production_history_sum = sum(history["milk_production"] for history in milking_history[:days_in_milk]) result, _ = quad( - MilkProduction.calculate_daily_milk_production, days_in_milk + 1, 305, args=(l_param, m_param, n_param)) + MilkProduction.calculate_daily_milk_production, days_in_milk + 1, 305, args=(l_param, m_param, n_param) + ) return result + production_history_sum diff --git a/tests/test_biophysical/test_animal/test_milk_production/test_milk_production.py b/tests/test_biophysical/test_animal/test_milk_production/test_milk_production.py index f3a72607d0..87818fb53c 100644 --- a/tests/test_biophysical/test_animal/test_milk_production/test_milk_production.py +++ b/tests/test_biophysical/test_animal/test_milk_production/test_milk_production.py @@ -202,7 +202,9 @@ def test_calculate_daily_milk_production( ) def test_calc_305_day_milk_yield(l_param: float, m_param: float, n_param: float, expected: float) -> None: """Test the calc_305_day_milk_yield method of the MilkProduction class.""" - assert MilkProduction.calculate_305_day_milk_yield(l_param, m_param, n_param, None) == pytest.approx(expected, rel=1e-6) + assert MilkProduction.calculate_305_day_milk_yield(l_param, m_param, n_param, None) == pytest.approx( + expected, rel=1e-6 + ) @pytest.mark.parametrize( From ba60e444740ba54de83bb6351a4fbe67b46e81da Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Tue, 25 Nov 2025 14:20:16 +0000 Subject: [PATCH 014/107] Update badges on README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a4b27a2974..2bb19e26cd 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-1717%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Flake8](https://img.shields.io/badge/Flake8-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Pytest](https://img.shields.io/badge/Pytest-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Coverage](https://img.shields.io/badge/Coverage-%25-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1718%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems From 9e4603f99c6d2ad6123391b211369f4d300c4c55 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Wed, 26 Nov 2025 00:41:00 +0900 Subject: [PATCH 015/107] checkout --- RUFAS/biophysical/animal/herd_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 864f766b2f..22c9e40f4a 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -688,7 +688,7 @@ def _check_if_cows_need_to_be_sold( MIN_DIM_FOR_REMOVAL = 60 animals_removed: list[Animal] = [] - while self.current_herd_size > self.herd_statistics.herd_num * self.selling_threshold and len(self.cows) > 0: + while len(self.cows) > self.herd_statistics.herd_num * self.selling_threshold and len(self.cows) > 0: # partitioning between dnb and non dnb cows dnb_indices: list[int] = [] non_dnb_indices: list[int] = [] @@ -764,7 +764,7 @@ def _check_if_replacement_heifers_needed(self, time: RufasTime) -> list[Animal]: """ animals_added: list[Animal] = [] while ( - self.current_herd_size + self.herd_statistics.bought_heifer_num + len(self.cows) < self.herd_statistics.herd_num * self.buying_threshold and time.simulation_day > 1 ): From 37414b13368178bae182bf1aa815ce2246038fbb Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Wed, 26 Nov 2025 22:53:59 +0900 Subject: [PATCH 016/107] Changes to the comparison logic --- RUFAS/biophysical/animal/herd_manager.py | 28 +++++++++++++++++++----- RUFAS/simulation_engine.py | 2 +- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 22c9e40f4a..f0b06571c5 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -606,6 +606,21 @@ def daily_routines( simulation_day=time.simulation_day, ) + + # removed_animals += self._check_if_cows_need_to_be_sold(simulation_day=time.simulation_day) + # newly_added_animals = self._check_if_replacement_heifers_needed(time=time) + # self._update_herd_structure( + # graduated_animals=graduated_animals, + # newborn_calves=newborn_calves, + # newly_added_animals=newly_added_animals, + # removed_animals=removed_animals, + # available_feeds=available_feeds, + # current_day_conditions=weather.get_current_day_conditions(time), + # total_inventory=total_inventory, + # simulation_day=time.simulation_day, + # ) + #print(newly_added_animals) + self.record_pen_history(time.simulation_day) enteric_methane_emission_by_pen: dict[str, float] = {} animal_manure_excretions_by_pen: dict[str, AnimalManureExcretions] = {} @@ -617,6 +632,7 @@ def daily_routines( self.update_herd_statistics() + AnimalModuleReporter.report_enteric_methane_emission(enteric_methane_emission_by_pen) AnimalModuleReporter.report_daily_animal_population(self.herd_statistics, time.simulation_day) AnimalModuleReporter.report_herd_statistics_data(self.herd_statistics, time.simulation_day) @@ -687,17 +703,15 @@ def _check_if_cows_need_to_be_sold( """ MIN_DIM_FOR_REMOVAL = 60 animals_removed: list[Animal] = [] - while len(self.cows) > self.herd_statistics.herd_num * self.selling_threshold and len(self.cows) > 0: + #print(str(simulation_day) + " sold") # partitioning between dnb and non dnb cows dnb_indices: list[int] = [] non_dnb_indices: list[int] = [] for index, cow in enumerate(self.cows): - if cow.days_in_milk < MIN_DIM_FOR_REMOVAL: - continue if cow.reproduction.do_not_breed: dnb_indices.append(index) - else: + elif cow.days_in_milk > MIN_DIM_FOR_REMOVAL: non_dnb_indices.append(index) if not dnb_indices and not non_dnb_indices: @@ -717,7 +731,8 @@ def _check_if_cows_need_to_be_sold( lowest_production_cow_index = 0 current_lowest_estimation_value = math.inf for index in non_dnb_indices: - estimated_production = self.cows[index].mature_equivalent_milking_prediction_305_day + # estimated_production = self.cows[index].mature_equivalent_milking_prediction_305_day + estimated_production = self.cows[index].milk_production.daily_milk_produced if estimated_production < current_lowest_estimation_value: current_lowest_estimation_value = estimated_production lowest_production_cow_index = index @@ -764,10 +779,11 @@ def _check_if_replacement_heifers_needed(self, time: RufasTime) -> list[Animal]: """ animals_added: list[Animal] = [] while ( - len(self.cows) + len(self.cows) + self.herd_statistics.bought_heifer_num < self.herd_statistics.herd_num * self.buying_threshold and time.simulation_day > 1 ): + print("cow: " + str(len(self.cows)) + "bought: " + str(self.herd_statistics.bought_heifer_num)) if len(self.replacement_market) == 0: break replacement = self.replacement_market.pop(0) diff --git a/RUFAS/simulation_engine.py b/RUFAS/simulation_engine.py index 1a5f978685..be905352d0 100644 --- a/RUFAS/simulation_engine.py +++ b/RUFAS/simulation_engine.py @@ -127,7 +127,7 @@ def _daily_simulation(self) -> None: is_time_to_reformulate_ration = self.time.current_date.date() == self.next_ration_reformulation if is_time_to_reformulate_ration: self._formulate_ration() - self.herd_manager.update_milk_305_day_yield_predictions() + # self.herd_manager.update_milk_305_day_yield_predictions() requested_feed = self.herd_manager.collect_daily_feed_request() self.feed_manager.report_feed_storage_levels(self.time.simulation_day, "daily_storage_levels") From 8066eb3c0fe2b867e91548e57cae550d55444989 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 26 Nov 2025 13:55:45 +0000 Subject: [PATCH 017/107] Apply Black Formatting --- RUFAS/biophysical/animal/herd_manager.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index f0b06571c5..f24f62dc4d 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -606,7 +606,6 @@ def daily_routines( simulation_day=time.simulation_day, ) - # removed_animals += self._check_if_cows_need_to_be_sold(simulation_day=time.simulation_day) # newly_added_animals = self._check_if_replacement_heifers_needed(time=time) # self._update_herd_structure( @@ -619,7 +618,7 @@ def daily_routines( # total_inventory=total_inventory, # simulation_day=time.simulation_day, # ) - #print(newly_added_animals) + # print(newly_added_animals) self.record_pen_history(time.simulation_day) enteric_methane_emission_by_pen: dict[str, float] = {} @@ -632,7 +631,6 @@ def daily_routines( self.update_herd_statistics() - AnimalModuleReporter.report_enteric_methane_emission(enteric_methane_emission_by_pen) AnimalModuleReporter.report_daily_animal_population(self.herd_statistics, time.simulation_day) AnimalModuleReporter.report_herd_statistics_data(self.herd_statistics, time.simulation_day) @@ -704,7 +702,7 @@ def _check_if_cows_need_to_be_sold( MIN_DIM_FOR_REMOVAL = 60 animals_removed: list[Animal] = [] while len(self.cows) > self.herd_statistics.herd_num * self.selling_threshold and len(self.cows) > 0: - #print(str(simulation_day) + " sold") + # print(str(simulation_day) + " sold") # partitioning between dnb and non dnb cows dnb_indices: list[int] = [] non_dnb_indices: list[int] = [] From 3c65eb774cbe25110c4f6e016fe12af3a89192c8 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Thu, 27 Nov 2025 00:50:12 +0900 Subject: [PATCH 018/107] Changes to the comparison logic --- RUFAS/biophysical/animal/herd_manager.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index f0b06571c5..d45138fba7 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -582,6 +582,7 @@ def daily_routines( sold_newborn_calves += sold_newborn_calves_from_cows newborn_calves += newborn_calves_from_cows born_calf_num = len(stillborn_newborn_calves + sold_newborn_calves + newborn_calves) + print(len(graduated_heiferIIIs), len(sold_and_died_cows), len(self.cows)) self.herd_statistics.born_calf_num = born_calf_num self._update_sold_animal_statistics( @@ -593,6 +594,8 @@ def daily_routines( self._update_stillborn_calf_statistics(stillborn_newborn_calves) if time.simulation_day > 0 and time.simulation_day % self.adjustment_period == 0: + #print("Balance---------------------------") + #print(len(graduated_heiferIIIs), len(sold_and_died_cows)) removed_animals += self._check_if_cows_need_to_be_sold(simulation_day=time.simulation_day) newly_added_animals = self._check_if_replacement_heifers_needed(time=time) self._update_herd_structure( @@ -631,6 +634,7 @@ def daily_routines( enteric_methane_emission_by_pen[f"{pen.animal_combination.name}_PEN_{pen.id}"] = pen.total_enteric_methane self.update_herd_statistics() + #print(len(self.cows)) AnimalModuleReporter.report_enteric_methane_emission(enteric_methane_emission_by_pen) @@ -783,7 +787,7 @@ def _check_if_replacement_heifers_needed(self, time: RufasTime) -> list[Animal]: < self.herd_statistics.herd_num * self.buying_threshold and time.simulation_day > 1 ): - print("cow: " + str(len(self.cows)) + "bought: " + str(self.herd_statistics.bought_heifer_num)) + # print("cow: " + str(len(self.cows)) + "bought: " + str(self.herd_statistics.bought_heifer_num)) if len(self.replacement_market) == 0: break replacement = self.replacement_market.pop(0) @@ -826,6 +830,8 @@ def _add_animal_to_new_array(self, animal: Animal) -> None: The animal object to be added to the respective array based on its `animal_type`. """ + if animal.animal_type is AnimalType.LAC_COW or animal.animal_type is AnimalType.DRY_COW: + print(str(len(self.cows)) + "before") animal_type_to_array_map: dict[AnimalType, list[Animal]] = { AnimalType.CALF: self.calves, AnimalType.HEIFER_I: self.heiferIs, @@ -836,6 +842,9 @@ def _add_animal_to_new_array(self, animal: Animal) -> None: } new_array = animal_type_to_array_map[animal.animal_type] new_array.append(animal) + if animal.animal_type is AnimalType.LAC_COW or animal.animal_type is AnimalType.DRY_COW: + print(str(len(new_array)) + "after") + def _update_animal_array(self, animal: Animal) -> None: """ From 3f01e6ffb0f3dceed589e5ed7f97d0cf7d7ddbef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 1 Dec 2025 09:45:01 +0000 Subject: [PATCH 019/107] Apply Black Formatting From f085873a040785dcec167b8ef5a83ea50e2500b1 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Mon, 1 Dec 2025 09:48:15 +0000 Subject: [PATCH 020/107] Update badges on README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8e50327a53..1ec771dc6c 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-1700%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Flake8](https://img.shields.io/badge/Flake8-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Pytest](https://img.shields.io/badge/Pytest-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Coverage](https://img.shields.io/badge/Coverage-%25-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1701%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems From 8210b1bce3b1028a23a8658ddd6aded205dc536b Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Mon, 1 Dec 2025 21:56:36 +0900 Subject: [PATCH 021/107] First fix --- RUFAS/biophysical/animal/herd_manager.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index d45138fba7..e2ffab7dce 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -608,6 +608,17 @@ def daily_routines( total_inventory=total_inventory, simulation_day=time.simulation_day, ) + else: + self._update_herd_structure( + graduated_animals=graduated_animals, + newborn_calves=newborn_calves, + newly_added_animals=[], + removed_animals=removed_animals, + available_feeds=available_feeds, + current_day_conditions=weather.get_current_day_conditions(time), + total_inventory=total_inventory, + simulation_day=time.simulation_day, + ) # removed_animals += self._check_if_cows_need_to_be_sold(simulation_day=time.simulation_day) From f9da875765ba5082df39f8283a2a0964b801a5c1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 1 Dec 2025 12:58:28 +0000 Subject: [PATCH 022/107] Apply Black Formatting --- RUFAS/biophysical/animal/herd_manager.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 716190d4a2..224e26a93c 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -594,8 +594,8 @@ def daily_routines( self._update_stillborn_calf_statistics(stillborn_newborn_calves) if time.simulation_day > 0 and time.simulation_day % self.adjustment_period == 0: - #print("Balance---------------------------") - #print(len(graduated_heiferIIIs), len(sold_and_died_cows)) + # print("Balance---------------------------") + # print(len(graduated_heiferIIIs), len(sold_and_died_cows)) removed_animals += self._check_if_cows_need_to_be_sold(simulation_day=time.simulation_day) newly_added_animals = self._check_if_replacement_heifers_needed(time=time) self._update_herd_structure( @@ -644,7 +644,7 @@ def daily_routines( enteric_methane_emission_by_pen[f"{pen.animal_combination.name}_PEN_{pen.id}"] = pen.total_enteric_methane self.update_herd_statistics() - #print(len(self.cows)) + # print(len(self.cows)) AnimalModuleReporter.report_enteric_methane_emission(enteric_methane_emission_by_pen) AnimalModuleReporter.report_daily_animal_population(self.herd_statistics, time.simulation_day) @@ -854,7 +854,6 @@ def _add_animal_to_new_array(self, animal: Animal) -> None: if animal.animal_type is AnimalType.LAC_COW or animal.animal_type is AnimalType.DRY_COW: print(str(len(new_array)) + "after") - def _update_animal_array(self, animal: Animal) -> None: """ Updates the internal animal array by removing the given animal from its current From b5cf1e41617507785c537378d35e69c548e975ba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 9 Dec 2025 14:43:37 +0000 Subject: [PATCH 023/107] Apply Black Formatting From eeb6135a5469984802e76cb8d4d92e2fd596d0a1 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Tue, 9 Dec 2025 14:46:49 +0000 Subject: [PATCH 024/107] Update badges on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ec771dc6c..df4701bf50 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![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-1701%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1703%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems From 209a845e1a47a941cbb6d98da54a86540b6e918c Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Thu, 11 Dec 2025 00:00:07 +0900 Subject: [PATCH 025/107] checkout --- input/data/config/example_open_lot_config.json | 2 +- input/task_manager_metadata.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/input/data/config/example_open_lot_config.json b/input/data/config/example_open_lot_config.json index d9d156d885..f0959fd7c1 100644 --- a/input/data/config/example_open_lot_config.json +++ b/input/data/config/example_open_lot_config.json @@ -1,7 +1,7 @@ { "start_date": "2013:1", "end_date": "2019:365", - "random_seed": 42, + "random_seed": 2615193842, "set_seed": true, "simulate_animals": true, "nutrient_standard": "NASEM", diff --git a/input/task_manager_metadata.json b/input/task_manager_metadata.json index ec954a0086..09fba52e38 100644 --- a/input/task_manager_metadata.json +++ b/input/task_manager_metadata.json @@ -3,7 +3,7 @@ "tasks": { "title": "Task manager data", "description": "Configuration file for general simulation parameters.", - "path": "input/data/tasks/example_freestall_task.json", + "path": "input/data/tasks/example_open_lot_task.json", "type": "json", "properties": "tasks_properties" }, From 14f610f28ccfac04d84f587546f5d185ddd5a69b Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Thu, 11 Dec 2025 15:35:09 +0900 Subject: [PATCH 026/107] Resolved failing seed issues --- RUFAS/biophysical/animal/herd_manager.py | 33 ++++--------------- .../data/config/example_open_lot_config.json | 2 +- input/task_manager_metadata.json | 2 +- 3 files changed, 8 insertions(+), 29 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 69a2b6506c..6a52bdadc8 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -586,7 +586,6 @@ def daily_routines( sold_newborn_calves += sold_newborn_calves_from_cows newborn_calves += newborn_calves_from_cows born_calf_num = len(stillborn_newborn_calves + sold_newborn_calves + newborn_calves) - print(len(graduated_heiferIIIs), len(sold_and_died_cows), len(self.cows)) self.herd_statistics.born_calf_num = born_calf_num self._update_sold_animal_statistics( @@ -596,12 +595,11 @@ def daily_routines( ) self._update_stillborn_calf_statistics(stillborn_newborn_calves) - if time.simulation_day > 0 and time.simulation_day % self.adjustment_period == 0: - # print("Balance---------------------------") - # print(len(graduated_heiferIIIs), len(sold_and_died_cows)) - removed_animals += self._check_if_cows_need_to_be_sold(simulation_day=time.simulation_day) + removed_animals += self._check_if_cows_need_to_be_sold(simulation_day=time.simulation_day, + removed_animal=removed_animals) newly_added_animals = self._check_if_replacement_heifers_needed(time=time) + self._update_herd_structure( graduated_animals=graduated_animals, newborn_calves=newborn_calves, @@ -624,20 +622,6 @@ def daily_routines( simulation_day=time.simulation_day, ) - # removed_animals += self._check_if_cows_need_to_be_sold(simulation_day=time.simulation_day) - # newly_added_animals = self._check_if_replacement_heifers_needed(time=time) - # self._update_herd_structure( - # graduated_animals=graduated_animals, - # newborn_calves=newborn_calves, - # newly_added_animals=newly_added_animals, - # removed_animals=removed_animals, - # available_feeds=available_feeds, - # current_day_conditions=weather.get_current_day_conditions(time), - # total_inventory=total_inventory, - # simulation_day=time.simulation_day, - # ) - # print(newly_added_animals) - self.record_pen_history(time.simulation_day) enteric_methane_emission_by_pen: dict[str, float] = {} animal_manure_excretions_by_pen: dict[str, AnimalManureExcretions] = {} @@ -648,7 +632,6 @@ def daily_routines( enteric_methane_emission_by_pen[f"{pen.animal_combination.name}_PEN_{pen.id}"] = pen.total_enteric_methane self.update_herd_statistics() - # print(len(self.cows)) AnimalModuleReporter.report_enteric_methane_emission(enteric_methane_emission_by_pen) AnimalModuleReporter.report_daily_animal_population(self.herd_statistics, time.simulation_day) @@ -710,6 +693,7 @@ def _create_newborn_calf(self, newborn_calf_config: NewBornCalfValuesTypedDict, def _check_if_cows_need_to_be_sold( self, simulation_day: int, + removed_animal: list[Animal] ) -> list[Animal]: """ Checks if surplus cows need to be sold based on herd size. @@ -721,11 +705,11 @@ def _check_if_cows_need_to_be_sold( MIN_DIM_FOR_REMOVAL = 60 animals_removed: list[Animal] = [] while len(self.cows) > self.herd_statistics.herd_num * self.selling_threshold and len(self.cows) > 0: - # print(str(simulation_day) + " sold") - # partitioning between dnb and non dnb cows dnb_indices: list[int] = [] non_dnb_indices: list[int] = [] for index, cow in enumerate(self.cows): + if cow in removed_animal: + continue if cow.reproduction.do_not_breed: dnb_indices.append(index) elif cow.days_in_milk > MIN_DIM_FOR_REMOVAL: @@ -800,7 +784,6 @@ def _check_if_replacement_heifers_needed(self, time: RufasTime) -> list[Animal]: < self.herd_statistics.herd_num * self.buying_threshold and time.simulation_day > 1 ): - # print("cow: " + str(len(self.cows)) + "bought: " + str(self.herd_statistics.bought_heifer_num)) if len(self.replacement_market) == 0: break replacement = self.replacement_market.pop(0) @@ -843,8 +826,6 @@ def _add_animal_to_new_array(self, animal: Animal) -> None: The animal object to be added to the respective array based on its `animal_type`. """ - if animal.animal_type is AnimalType.LAC_COW or animal.animal_type is AnimalType.DRY_COW: - print(str(len(self.cows)) + "before") animal_type_to_array_map: dict[AnimalType, list[Animal]] = { AnimalType.CALF: self.calves, AnimalType.HEIFER_I: self.heiferIs, @@ -855,8 +836,6 @@ def _add_animal_to_new_array(self, animal: Animal) -> None: } new_array = animal_type_to_array_map[animal.animal_type] new_array.append(animal) - if animal.animal_type is AnimalType.LAC_COW or animal.animal_type is AnimalType.DRY_COW: - print(str(len(new_array)) + "after") def _update_animal_array(self, animal: Animal) -> None: """ diff --git a/input/data/config/example_open_lot_config.json b/input/data/config/example_open_lot_config.json index f0959fd7c1..d9d156d885 100644 --- a/input/data/config/example_open_lot_config.json +++ b/input/data/config/example_open_lot_config.json @@ -1,7 +1,7 @@ { "start_date": "2013:1", "end_date": "2019:365", - "random_seed": 2615193842, + "random_seed": 42, "set_seed": true, "simulate_animals": true, "nutrient_standard": "NASEM", diff --git a/input/task_manager_metadata.json b/input/task_manager_metadata.json index 09fba52e38..ec954a0086 100644 --- a/input/task_manager_metadata.json +++ b/input/task_manager_metadata.json @@ -3,7 +3,7 @@ "tasks": { "title": "Task manager data", "description": "Configuration file for general simulation parameters.", - "path": "input/data/tasks/example_open_lot_task.json", + "path": "input/data/tasks/example_freestall_task.json", "type": "json", "properties": "tasks_properties" }, From 81c374fd6bbbeaf6efc1908e57d54ea65ceb0f87 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 11 Dec 2025 06:37:07 +0000 Subject: [PATCH 027/107] Apply Black Formatting --- RUFAS/biophysical/animal/herd_manager.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 6a52bdadc8..cd0f24a241 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -596,8 +596,9 @@ def daily_routines( self._update_stillborn_calf_statistics(stillborn_newborn_calves) if time.simulation_day > 0 and time.simulation_day % self.adjustment_period == 0: - removed_animals += self._check_if_cows_need_to_be_sold(simulation_day=time.simulation_day, - removed_animal=removed_animals) + removed_animals += self._check_if_cows_need_to_be_sold( + simulation_day=time.simulation_day, removed_animal=removed_animals + ) newly_added_animals = self._check_if_replacement_heifers_needed(time=time) self._update_herd_structure( @@ -690,11 +691,7 @@ def _create_newborn_calf(self, newborn_calf_config: NewBornCalfValuesTypedDict, newborn_calf.events.add_event(newborn_calf.days_born, simulation_day, animal_constants.ENTER_HERD) return newborn_calf - def _check_if_cows_need_to_be_sold( - self, - simulation_day: int, - removed_animal: list[Animal] - ) -> list[Animal]: + def _check_if_cows_need_to_be_sold(self, simulation_day: int, removed_animal: list[Animal]) -> list[Animal]: """ Checks if surplus cows need to be sold based on herd size. From 774713ebdb47be3fea5174ee828bb7820c19de86 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Thu, 11 Dec 2025 06:40:24 +0000 Subject: [PATCH 028/107] Update badges on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index df4701bf50..5cc4d1ba85 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![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-1703%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1704%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems From 44f3d2193d166becfa5b63d67364bc8cc97676a5 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Thu, 11 Dec 2025 17:07:18 +0900 Subject: [PATCH 029/107] updated properties --- input/metadata/properties/default.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/input/metadata/properties/default.json b/input/metadata/properties/default.json index 92ace3d570..d53eefdc87 100644 --- a/input/metadata/properties/default.json +++ b/input/metadata/properties/default.json @@ -98,8 +98,8 @@ "herd_buying_threshold": { "type": "number", "description": "The threshold that should buy animals to maintain herd size.", - "default": 1.01, - "minimum": 1 + "default": 0.98, + "minimum": 0.0 }, "breed": { "type": "string", From c70f11b7760faa8fb241bbac356ef5875088040c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 11 Dec 2025 08:09:15 +0000 Subject: [PATCH 030/107] Apply Black Formatting From eeee541c8f56c2dc3c9c5a173a20598291f9acf4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 18 Dec 2025 15:09:35 +0000 Subject: [PATCH 031/107] Apply Black Formatting From 8b0403e796f17623f3f504a587302a4773c38fac Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Thu, 18 Dec 2025 15:12:56 +0000 Subject: [PATCH 032/107] Update badges on README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b68698b311..7a9746a036 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-1727%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Flake8](https://img.shields.io/badge/Flake8-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Pytest](https://img.shields.io/badge/Pytest-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Coverage](https://img.shields.io/badge/Coverage-%25-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1715%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems From 188c76b8d59a58f10578ba4c816d28ae3826ca5d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Jan 2026 17:47:47 +0000 Subject: [PATCH 033/107] Apply Black Formatting From 16dcaca328af386a1df2be75cbbbf83384a145e8 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Tue, 6 Jan 2026 17:50:51 +0000 Subject: [PATCH 034/107] Update badges on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dc548fcc5b..2d553acb4b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![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-1715%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1714%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems **RuFaS** is an open-source, next-generation, whole-farm modeling environment that simulates dairy farm production and environmental impact. It is designed to support research, innovation, and sustainable decision-making in ruminant animal agriculture. From 84d7fba24058b0e59b23778094fe5d33e3a0eecb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 20 Jan 2026 03:05:30 +0000 Subject: [PATCH 035/107] Apply Black Formatting --- RUFAS/biophysical/animal/animal.py | 4 ++-- RUFAS/biophysical/animal/herd_manager.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/RUFAS/biophysical/animal/animal.py b/RUFAS/biophysical/animal/animal.py index 14f49b0fdb..61b485c349 100644 --- a/RUFAS/biophysical/animal/animal.py +++ b/RUFAS/biophysical/animal/animal.py @@ -1575,8 +1575,8 @@ def daily_routines(self, time: RufasTime) -> DailyRoutinesOutput: newborn_calf_config, daily_routines_output.herd_reproduction_statistics = self.daily_reproduction_update(time) - (daily_routines_output.animal_status, daily_routines_output.newborn_calf_config) = ( - self.animal_life_stage_update(time) + daily_routines_output.animal_status, daily_routines_output.newborn_calf_config = self.animal_life_stage_update( + time ) if self.animal_type.is_cow and newborn_calf_config is not None: diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 6965d4dfb5..8748a57f38 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -160,7 +160,7 @@ def __init__( if self.simulate_animals: herd_population = HerdFactory.post_animal_population - (self.calves, self.heiferIs, self.heiferIIs, self.heiferIIIs, self.cows, self.replacement_market) = ( + self.calves, self.heiferIs, self.heiferIIs, self.heiferIIIs, self.cows, self.replacement_market = ( herd_population.calves, herd_population.heiferIs, herd_population.heiferIIs, From c824f884067926c013db27e9c17374ec4ccfe471 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Tue, 20 Jan 2026 03:08:55 +0000 Subject: [PATCH 036/107] Update badges on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2d553acb4b..87addf75ab 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![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-1714%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1713%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems **RuFaS** is an open-source, next-generation, whole-farm modeling environment that simulates dairy farm production and environmental impact. It is designed to support research, innovation, and sustainable decision-making in ruminant animal agriculture. From eb642912b4893c3d8ed5e6d0036a5f5d665e3b24 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Wed, 21 Jan 2026 06:40:01 +0900 Subject: [PATCH 037/107] Added required herd info modification --- RUFAS/biophysical/animal/herd_manager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index cd0f24a241..583e8a87f8 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -753,6 +753,8 @@ def _check_if_cows_need_to_be_sold(self, simulation_day: int, removed_animal: li ) self.herd_statistics.cow_num -= 1 self.herd_statistics.sold_cow_oversupply_num += 1 + self.herd_statistics.cow_herd_exit_num -= 1 + self.herd_statistics.sold_cow_num += 1 return animals_removed From a8a4507179063133c018f6708ca8057b9adc3a21 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Thu, 22 Jan 2026 19:12:33 +0900 Subject: [PATCH 038/107] Modified tests for cow selling logic --- .../test_herd_manager_daily_routines.py | 139 +++++++++++++----- 1 file changed, 102 insertions(+), 37 deletions(-) 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 3c45a0c20c..e5c662421a 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 @@ -453,7 +453,7 @@ def test_daily_routines(herd_manager: HerdManager, mock_herd: dict[str, list[Ani ) assert mock_check_if_cows_need_to_be_sold.call_count == 0 assert mock_check_if_replacement_heifers_needed.call_count == 0 - assert mock_update_herd_structure.call_count == 0 + assert mock_update_herd_structure.call_count == 1 mock_record_pen_history.assert_called_once_with(mock_time.simulation_day) mock_update_herd_statistics.assert_called_once_with() mock_report_manure_streams.assert_called_once() @@ -496,44 +496,109 @@ def test_create_newborn_calf( animal.events.add_event.assert_called_once() -def test_check_if_cows_need_to_be_sold( - mock_get_data_side_effect: list[Any], mocker: MockerFixture, mock_herd: dict[str, list[Animal]] +def _create_sortable_mock_cow( + id_val: int, is_dnb: bool, daily_milk: float, days_in_milk: int +) -> MagicMock: + """Helper to create a mock cow with specific sorting attributes.""" + cow = MagicMock(spec=Animal) + cow.id = id_val + cow.animal_type = AnimalType.LAC_COW + cow.body_weight = 600.0 + cow.sold_at_day = None + + cow.reproduction = MagicMock() + cow.reproduction.do_not_breed = is_dnb + cow.reproduction.calves = 1 + + cow.milk_production = MagicMock() + cow.milk_production.daily_milk_produced = daily_milk + + cow.days_in_milk = days_in_milk + return cow + + +def test_check_if_cows_need_to_be_sold_comprehensive( + herd_manager: HerdManager, mocker: MockerFixture ) -> None: - """Unit test for _check_if_heifers_need_to_be_sold()""" - herd_manager, _ = mock_herd_manager( - calves=mock_herd["calves"], - heiferIs=mock_herd["heiferIs"], - heiferIIs=mock_herd["heiferIIs"], - heiferIIIs=mock_herd["heiferIIIs"] * 25, - cows=mock_herd["dry_cows"] + mock_herd["lac_cows"], - replacement=mock_herd["replacement"], - mocker=mocker, - mock_get_data_side_effect=mock_get_data_side_effect, - ) - herd_manager.herd_statistics.heiferIII_num, herd_manager.herd_statistics.cow_num = ( - len(herd_manager.heiferIIIs), - len(herd_manager.cows), - ) + """ + Unit test for _check_if_cows_need_to_be_sold(). + + Verifies: + 1. Cows marked 'do_not_breed' (DNB) are removed first. + 2. Within priority groups, cows are removed by lowest milk production. + 3. Non-DNB cows with DIM < 60 are protected (skipped). + 4. Error is logged if herd is too large but no cows are eligible. + 5. Statistics are updated correctly based on source code logic. + """ + HERD_TARGET = 10 + SELLING_THRESHOLD = 1.0 + SIMULATION_DAY = 100 - result = herd_manager._check_if_cows_need_to_be_sold(simulation_day=0) - - expected_sold_heiferIIIs = mock_herd["heiferIIIs"][::-1][:3] - expected_sold_heiferIIIs_info = [ - { - "id": removed_heiferIII.id, - "animal_type": removed_heiferIII.animal_type.value, - "sold_at_day": removed_heiferIII.sold_at_day, - "body_weight": removed_heiferIII.body_weight, - "cull_reason": "NA", - "days_in_milk": "NA", - "parity": "NA", - } - for removed_heiferIII in expected_sold_heiferIIIs[:3] - ] - assert result == expected_sold_heiferIIIs - assert herd_manager.herd_statistics.sold_heiferIIIs_info == expected_sold_heiferIIIs_info - assert herd_manager.herd_statistics.heiferIII_num == 97 - assert herd_manager.herd_statistics.sold_cow_oversupply_num == 3 + herd_manager.herd_statistics.herd_num = HERD_TARGET + herd_manager.selling_threshold = SELLING_THRESHOLD + herd_manager.herd_statistics.cow_num = 15 + herd_manager.herd_statistics.sold_cow_oversupply_num = 0 + herd_manager.herd_statistics.sold_cow_num = 0 + herd_manager.herd_statistics.cow_herd_exit_num = 10 + + cow_dnb_low_milk = _create_sortable_mock_cow(1, True, 10.0, 100) + cow_dnb_high_milk = _create_sortable_mock_cow(2, True, 50.0, 100) + cow_normal_low_milk = _create_sortable_mock_cow(3, False, 20.0, 100) + cow_normal_high_milk = _create_sortable_mock_cow(4, False, 40.0, 100) + cow_protected_low_dim = _create_sortable_mock_cow(5, False, 5.0, 10) + + fillers = [] + for i in range(10): + fillers.append(_create_sortable_mock_cow(10 + i, False, 100.0, 200)) + + fillers[0].milk_production.daily_milk_produced = 90.0 + + all_cows = [ + cow_dnb_low_milk, + cow_dnb_high_milk, + cow_normal_low_milk, + cow_normal_high_milk, + cow_protected_low_dim + ] + fillers + + herd_manager.cows = all_cows + + mock_om_add_error = mocker.patch.object(herd_manager.om, "add_error") + + removed_cows = herd_manager._check_if_cows_need_to_be_sold(SIMULATION_DAY, []) + + assert len(removed_cows) == 5 + assert len(herd_manager.cows) == 10 + + assert removed_cows[0] == cow_dnb_low_milk + assert removed_cows[1] == cow_dnb_high_milk + assert removed_cows[2] == cow_normal_low_milk + assert removed_cows[3] == cow_normal_high_milk + assert removed_cows[4] == fillers[0] + + assert cow_protected_low_dim in herd_manager.cows + + assert herd_manager.herd_statistics.sold_cow_oversupply_num == 5 + assert herd_manager.herd_statistics.sold_cow_num == 5 + assert herd_manager.herd_statistics.cow_herd_exit_num == 5 + + assert len(herd_manager.herd_statistics.sold_cows_info) == 5 + assert herd_manager.herd_statistics.sold_cows_info[0]["id"] == cow_dnb_low_milk.id + assert herd_manager.herd_statistics.sold_cows_info[0]["sold_at_day"] == SIMULATION_DAY + + herd_manager.herd_statistics.herd_num = 1 + herd_manager.selling_threshold = 1.0 + + cow_prot_1 = _create_sortable_mock_cow(99, False, 100.0, 10) + cow_prot_2 = _create_sortable_mock_cow(98, False, 100.0, 10) + herd_manager.cows = [cow_prot_1, cow_prot_2] + + stuck_result = herd_manager._check_if_cows_need_to_be_sold(SIMULATION_DAY, []) + + assert len(stuck_result) == 0 + mock_om_add_error.assert_called_once_with( + "Unable to adjust herd size", "There are no cow that's qualified to be sold.", {} + ) def test_check_if_replacement_heifers_needed( From d2d6b28f4430308c5049d92873ca96bc58ec1da5 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Thu, 22 Jan 2026 19:29:11 +0900 Subject: [PATCH 039/107] Modified tests for cow selling logic --- RUFAS/biophysical/animal/herd_manager.py | 2 -- .../test_herd_manager/test_herd_manager_initialization.py | 5 ++++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 06b47b5d69..0a9960255b 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -92,8 +92,6 @@ def __init__( True if user-defined rations are used for the herd, otherwise false. available_feeds : list[Feed] Nutrition information of feeds available to formulate animals rations with. - feed_emissions_estimator : PurchasedFeedEmissionsEstimator, default=None - Instance of the PurchasedFeedEmissionsEstimator class. """ self.im = InputManager() diff --git a/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_initialization.py b/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_initialization.py index f1c56e11ac..50e9e76053 100644 --- a/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_initialization.py +++ b/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_initialization.py @@ -100,7 +100,10 @@ def test_init_uses_set_ration_feeds_when_not_user_defined(mocker: MockerFixture) } animal_data: dict[str, Any] = { - "herd_information": {"herd_num": 123}, + "herd_information": {"herd_num": 123, + "herd_size_adjustment_period": 30, + "herd_selling_threshold": 1.01, + "herd_buying_threshold": 1.06}, "housing": "barn", "pasture_concentrate": 0, "ration": {"formulation_interval": 7, "maximum_ration_reformulation_attempts": 250}, From 87c6828aa6825bee09ab8cdb5e4d2bf4fc9ab04c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 22 Jan 2026 10:34:06 +0000 Subject: [PATCH 040/107] Apply Black Formatting --- .../test_herd_manager_daily_routines.py | 10 +++------- .../test_herd_manager_initialization.py | 10 ++++++---- 2 files changed, 9 insertions(+), 11 deletions(-) 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 e5c662421a..38329d7b3b 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 @@ -496,9 +496,7 @@ def test_create_newborn_calf( animal.events.add_event.assert_called_once() -def _create_sortable_mock_cow( - id_val: int, is_dnb: bool, daily_milk: float, days_in_milk: int -) -> MagicMock: +def _create_sortable_mock_cow(id_val: int, is_dnb: bool, daily_milk: float, days_in_milk: int) -> MagicMock: """Helper to create a mock cow with specific sorting attributes.""" cow = MagicMock(spec=Animal) cow.id = id_val @@ -517,9 +515,7 @@ def _create_sortable_mock_cow( return cow -def test_check_if_cows_need_to_be_sold_comprehensive( - herd_manager: HerdManager, mocker: MockerFixture -) -> None: +def test_check_if_cows_need_to_be_sold_comprehensive(herd_manager: HerdManager, mocker: MockerFixture) -> None: """ Unit test for _check_if_cows_need_to_be_sold(). @@ -558,7 +554,7 @@ def test_check_if_cows_need_to_be_sold_comprehensive( cow_dnb_high_milk, cow_normal_low_milk, cow_normal_high_milk, - cow_protected_low_dim + cow_protected_low_dim, ] + fillers herd_manager.cows = all_cows diff --git a/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_initialization.py b/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_initialization.py index 50e9e76053..de4336e26f 100644 --- a/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_initialization.py +++ b/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_initialization.py @@ -100,10 +100,12 @@ def test_init_uses_set_ration_feeds_when_not_user_defined(mocker: MockerFixture) } animal_data: dict[str, Any] = { - "herd_information": {"herd_num": 123, - "herd_size_adjustment_period": 30, - "herd_selling_threshold": 1.01, - "herd_buying_threshold": 1.06}, + "herd_information": { + "herd_num": 123, + "herd_size_adjustment_period": 30, + "herd_selling_threshold": 1.01, + "herd_buying_threshold": 1.06, + }, "housing": "barn", "pasture_concentrate": 0, "ration": {"formulation_interval": 7, "maximum_ration_reformulation_attempts": 250}, From 17091fb3351c67e4cb96222289988a527a8834ee Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Thu, 22 Jan 2026 10:37:54 +0000 Subject: [PATCH 041/107] Update badges on README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 87addf75ab..a0a0b44606 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![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-1713%20errors-red)](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-1685%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems **RuFaS** is an open-source, next-generation, whole-farm modeling environment that simulates dairy farm production and environmental impact. It is designed to support research, innovation, and sustainable decision-making in ruminant animal agriculture. From f534bc2a6e0f3e7a7d6e420110e6f2392a19b306 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Fri, 23 Jan 2026 13:57:39 +0900 Subject: [PATCH 042/107] Fixed flake8 --- RUFAS/biophysical/animal/herd_manager.py | 110 +++++++++++------------ 1 file changed, 53 insertions(+), 57 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 0a9960255b..881469b8d9 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -692,73 +692,69 @@ def _create_newborn_calf(self, newborn_calf_config: NewBornCalfValuesTypedDict, newborn_calf.events.add_event(newborn_calf.days_born, simulation_day, animal_constants.ENTER_HERD) return newborn_calf - def _check_if_cows_need_to_be_sold(self, simulation_day: int, removed_animal: list[Animal]) -> list[Animal]: - """ - Checks if surplus cows need to be sold based on herd size. - - Rule: - - Prefer removing cows with reproduction.do_not_breed == True, ordered by lowest daily milk. - - If none, remove non-DNB cows ordered by lowest 305-day milk. - """ + def _get_cow_removal_index(self, removed_animal: list[Animal]) -> int | None: + """Finds the index of the best candidate cow to sell based on priority rules.""" MIN_DIM_FOR_REMOVAL = 60 + dnb_indices = [] + non_dnb_indices = [] + + for index, cow in enumerate(self.cows): + if cow in removed_animal: + continue + if cow.reproduction.do_not_breed: + dnb_indices.append(index) + elif cow.days_in_milk > MIN_DIM_FOR_REMOVAL: + non_dnb_indices.append(index) + + if not dnb_indices and not non_dnb_indices: + return None + + # Priority 1: DNB cows by lowest daily milk + if dnb_indices: + return min(dnb_indices, key=lambda i: self.cows[i].milk_production.daily_milk_produced) + + # Priority 2: Non-DNB cows by lowest daily milk (qualified by DIM) + return min(non_dnb_indices, key=lambda i: self.cows[i].milk_production.daily_milk_produced) + + def _record_sold_cow_stats(self, removed_cow: Animal, simulation_day: int) -> None: + """Updates herd statistics and metadata for a sold cow.""" + removed_cow.sold_at_day = simulation_day + self.herd_statistics.sold_cows_info.append( + SoldAnimalTypedDict( + id=removed_cow.id, + animal_type=removed_cow.animal_type.value, + sold_at_day=removed_cow.sold_at_day, + body_weight=removed_cow.body_weight, + cull_reason="NA", + days_in_milk=removed_cow.days_in_milk, + parity="NA", + ) + ) + self.herd_statistics.cow_num -= 1 + self.herd_statistics.sold_cow_oversupply_num += 1 + self.herd_statistics.cow_herd_exit_num -= 1 + self.herd_statistics.sold_cow_num += 1 + + def _check_if_cows_need_to_be_sold(self, simulation_day: int, removed_animal: list[Animal]) -> list[Animal]: + """Checks if surplus cows need to be sold based on herd size.""" animals_removed: list[Animal] = [] + while len(self.cows) > self.herd_statistics.herd_num * self.selling_threshold and len(self.cows) > 0: - dnb_indices: list[int] = [] - non_dnb_indices: list[int] = [] - for index, cow in enumerate(self.cows): - if cow in removed_animal: - continue - if cow.reproduction.do_not_breed: - dnb_indices.append(index) - elif cow.days_in_milk > MIN_DIM_FOR_REMOVAL: - non_dnb_indices.append(index) - - if not dnb_indices and not non_dnb_indices: - self.om.add_error("Unable to adjust herd size", "There are no cow that's qualified to be sold.", {}) - break + remove_index = self._get_cow_removal_index(removed_animal) - if dnb_indices: # when there is dnb to remove - lowest_production_cow_index = 0 - current_lowest_production_value = math.inf - for index in dnb_indices: - daily_milk_produced = self.cows[index].milk_production.daily_milk_produced - if daily_milk_produced < current_lowest_production_value: - current_lowest_production_value = daily_milk_produced - lowest_production_cow_index = index - remove_index = lowest_production_cow_index - else: - lowest_production_cow_index = 0 - current_lowest_estimation_value = math.inf - for index in non_dnb_indices: - # estimated_production = self.cows[index].mature_equivalent_milking_prediction_305_day - estimated_production = self.cows[index].milk_production.daily_milk_produced - if estimated_production < current_lowest_estimation_value: - current_lowest_estimation_value = estimated_production - lowest_production_cow_index = index - remove_index = lowest_production_cow_index + if remove_index is None: + self.om.add_error("Unable to adjust herd size", + "There are no cow that's qualified to be sold.", {}) + break removed_cow = self.cows.pop(remove_index) - removed_cow.sold_at_day = simulation_day + self._record_sold_cow_stats(removed_cow, simulation_day) animals_removed.append(removed_cow) - self.herd_statistics.sold_cows_info.append( - SoldAnimalTypedDict( - id=removed_cow.id, - animal_type=removed_cow.animal_type.value, - sold_at_day=removed_cow.sold_at_day, - body_weight=removed_cow.body_weight, - cull_reason="NA", - days_in_milk=removed_cow.days_in_milk, - parity="NA", - ) - ) - self.herd_statistics.cow_num -= 1 - self.herd_statistics.sold_cow_oversupply_num += 1 - self.herd_statistics.cow_herd_exit_num -= 1 - self.herd_statistics.sold_cow_num += 1 - return animals_removed + # estimated_production = self.cows[index].mature_equivalent_milking_prediction_305_day + def _check_if_replacement_heifers_needed(self, time: RufasTime) -> list[Animal]: """ Checks if replacement heifers are needed to maintain the herd size. From 73c823c0d800f1b0e2faf6c35caced09b2bf738b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 23 Jan 2026 04:59:37 +0000 Subject: [PATCH 043/107] Apply Black Formatting From c7d5dd2b52d25620258c56cfa93faf158125d1a1 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Fri, 23 Jan 2026 21:57:45 +0900 Subject: [PATCH 044/107] Removed unused lines --- RUFAS/biophysical/animal/herd_manager.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 881469b8d9..81766e3755 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -753,8 +753,6 @@ def _check_if_cows_need_to_be_sold(self, simulation_day: int, removed_animal: li return animals_removed - # estimated_production = self.cows[index].mature_equivalent_milking_prediction_305_day - def _check_if_replacement_heifers_needed(self, time: RufasTime) -> list[Animal]: """ Checks if replacement heifers are needed to maintain the herd size. From 81e7ff7fd33736942527f8b8f2777fc03c2781c0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 23 Jan 2026 13:59:03 +0000 Subject: [PATCH 045/107] Apply Black Formatting --- RUFAS/biophysical/animal/herd_manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 81766e3755..4ce5db9be5 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -743,8 +743,7 @@ def _check_if_cows_need_to_be_sold(self, simulation_day: int, removed_animal: li remove_index = self._get_cow_removal_index(removed_animal) if remove_index is None: - self.om.add_error("Unable to adjust herd size", - "There are no cow that's qualified to be sold.", {}) + self.om.add_error("Unable to adjust herd size", "There are no cow that's qualified to be sold.", {}) break removed_cow = self.cows.pop(remove_index) From d5b90baf368f7f22544272fd0aca53d4c8f452ea Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Fri, 23 Jan 2026 23:06:22 +0900 Subject: [PATCH 046/107] Removed ME305 --- .../animal/milk/lactation_curve.py | 2 +- .../animal/milk/milk_production.py | 22 +++---------------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/RUFAS/biophysical/animal/milk/lactation_curve.py b/RUFAS/biophysical/animal/milk/lactation_curve.py index 343f8791d8..204c459d25 100644 --- a/RUFAS/biophysical/animal/milk/lactation_curve.py +++ b/RUFAS/biophysical/animal/milk/lactation_curve.py @@ -377,7 +377,7 @@ def _estimate_305_day_milk_yield_by_parity( @staticmethod def _calculate_305_day_milk_yield_error(l_param: float, m_param: float, n_param: float, milk_yield: float) -> float: """Calculates absolute difference between an estimated 305 day milk yield and a predetermined one.""" - return abs(MilkProduction.calculate_305_day_milk_yield(l_param, m_param, n_param, None) - milk_yield) + return abs(MilkProduction.calc_305_day_milk_yield(l_param, m_param, n_param) - milk_yield) @classmethod def _fit_wood_l_param_to_milk_yield( diff --git a/RUFAS/biophysical/animal/milk/milk_production.py b/RUFAS/biophysical/animal/milk/milk_production.py index fc9ecf17a0..9adebbbb25 100644 --- a/RUFAS/biophysical/animal/milk/milk_production.py +++ b/RUFAS/biophysical/animal/milk/milk_production.py @@ -260,13 +260,7 @@ def calculate_daily_milk_production(days_in_milk: int, l_param: float, m_param: return l_param * np.power(days_in_milk, m_param) * np.exp(-1 * n_param * days_in_milk) @staticmethod - def calculate_305_day_milk_yield( - l_param: float, - m_param: float, - n_param: float, - milking_history: list[MilkProductionRecord] | None, - days_in_milk: int = 0, - ) -> float: + def calc_305_day_milk_yield(l_param: float, m_param: float, n_param: float) -> float: """ Calculates the total milk yield from day 1 to day 305 of the lactation. @@ -282,10 +276,6 @@ def calculate_305_day_milk_yield( Wood's lactation curve parameter m. n_param: float Wood's lactation curve parameter n. - days_in_milk : int - Days in milk. - milking_history : - The milk production history if the animal. Returns ------- @@ -293,15 +283,9 @@ def calculate_305_day_milk_yield( 305 day milk yield for a cow with the given lactation curve (kg). """ - production_history_sum = 0 - if 305 > days_in_milk > 0 and milking_history is not None: - production_history_sum = sum(history["milk_production"] for history in milking_history[:days_in_milk]) - - result, _ = quad( - MilkProduction.calculate_daily_milk_production, days_in_milk + 1, 305, args=(l_param, m_param, n_param) - ) - return result + production_history_sum + result, _ = quad(MilkProduction.calculate_daily_milk_production, 1, 305, args=(l_param, m_param, n_param)) + return result def _get_milk_production_adjustment(self) -> float: """ From 7b423eef3c45e7063c99cc4f4cce15779ac05cd5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 23 Jan 2026 14:08:14 +0000 Subject: [PATCH 047/107] Apply Black Formatting From 5ca9b85ac3319049ed0edfb068047ac8f82d2ea9 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Fri, 23 Jan 2026 23:08:37 +0900 Subject: [PATCH 048/107] Removed ME305 --- RUFAS/biophysical/animal/animal.py | 64 ++++++++---------------------- 1 file changed, 16 insertions(+), 48 deletions(-) diff --git a/RUFAS/biophysical/animal/animal.py b/RUFAS/biophysical/animal/animal.py index 61b485c349..e69c2bdf78 100644 --- a/RUFAS/biophysical/animal/animal.py +++ b/RUFAS/biophysical/animal/animal.py @@ -14,11 +14,7 @@ from RUFAS.biophysical.animal.data_types.daily_routines_output import DailyRoutinesOutput from RUFAS.biophysical.animal.data_types.digestive_system import DigestiveSystemInputs from RUFAS.biophysical.animal.data_types.growth import GrowthInputs, GrowthOutputs -from RUFAS.biophysical.animal.data_types.milk_production import ( - MilkProductionInputs, - MilkProductionOutputs, - MilkProductionStatistics, -) +from RUFAS.biophysical.animal.data_types.milk_production import MilkProductionInputs, MilkProductionOutputs from RUFAS.biophysical.animal.data_types.nutrients import NutrientsInputs from RUFAS.biophysical.animal.data_types.nutrition_data_structures import NutritionRequirements, NutritionSupply from RUFAS.biophysical.animal.data_types.pen_history import PenHistory @@ -201,8 +197,6 @@ def __init__( self.nutrition_supply: NutritionSupply = NutritionSupply.make_empty_nutrition_supply() self.nutrition_supply.dry_matter = AnimalModuleConstants.DEFAULT_DRY_MATTER_INTAKE self.previous_nutrition_supply: NutritionSupply | None = None - self.milk_yield_305_day: float = 0.0 - self.mature_equivalent_milking_prediction_305_day: float = 0.0 self._days_in_milk: int = 0 self._milk_production_output_days_in_milk: int = 0 @@ -1079,22 +1073,6 @@ def dead(self) -> bool: """ return True if (self.dead_at_day is not None and self.dead_at_day >= 0) else False - @property - def milk_statistics(self) -> MilkProductionStatistics: - """Returns the milk statistics for the animal.""" - if not self.animal_type.is_cow: - raise TypeError() - return MilkProductionStatistics( - cow_id=self.id, - pen_id=self.pen_history[-1]["pen"], - days_in_milk=self.days_in_milk, - estimated_daily_milk_produced=self.milk_production.daily_milk_produced, - milk_protein=self.milk_production.true_protein_content, - milk_fat=self.milk_production.fat_content, - milk_lactose=self.milk_production.lactose_content, - parity=self.calves, - ) - def _assign_sex_to_newborn_calf(self) -> None: """ Assign a sex to a newborn calf based on the semen type and male calf rate. @@ -1317,7 +1295,7 @@ def _daily_nutrients_update(self) -> None: ) self.nutrients.perform_daily_phosphorus_update(nutrients_inputs) - def _daily_digestive_system_update(self) -> None: + def _daily_digestive_system_update(self) -> dict[AnimalType, dict[str, float]]: """ Performs the daily digestive system updates for the animal. @@ -1342,7 +1320,8 @@ def _daily_digestive_system_update(self) -> None: fat_content=MilkProduction.fat_percent, protein_content=self.milk_production.true_protein_content, ) - self.digestive_system.process_digestion(digestive_system_inputs) + digestion_output = self.digestive_system.process_digestion(digestive_system_inputs) + return digestion_output def daily_milking_update(self, time: RufasTime) -> None: """ @@ -1567,7 +1546,8 @@ def daily_routines(self, time: RufasTime) -> DailyRoutinesOutput: self._daily_nutrients_update() - self._daily_digestive_system_update() + digestion_outputs = self._daily_digestive_system_update() + daily_routines_output.daily_digestion_output = digestion_outputs self.daily_milking_update(time) @@ -1575,8 +1555,8 @@ def daily_routines(self, time: RufasTime) -> DailyRoutinesOutput: newborn_calf_config, daily_routines_output.herd_reproduction_statistics = self.daily_reproduction_update(time) - daily_routines_output.animal_status, daily_routines_output.newborn_calf_config = self.animal_life_stage_update( - time + (daily_routines_output.animal_status, daily_routines_output.newborn_calf_config) = ( + self.animal_life_stage_update(time) ) if self.animal_type.is_cow and newborn_calf_config is not None: @@ -1750,6 +1730,14 @@ def animal_life_stage_update(self, time: RufasTime) -> tuple[AnimalStatus, NewBo self.cull_reason = animal_constants.DEATH_CULL animal_status = AnimalStatus.DEAD + if ( + self.animal_type.is_cow + and self.reproduction.do_not_breed + and self.milk_production.daily_milk_produced < AnimalConfig.cull_milk_production + ): + self.cull_reason = animal_constants.LOW_PROD_CULL + self.sold_at_day = time.simulation_day + animal_status = AnimalStatus.SOLD return animal_status, newborn_calf_config def _evaluate_calf_for_heiferI(self) -> bool: @@ -2361,23 +2349,3 @@ def calculate_nutrition_requirements( ) return requirements - - def update_mature_equivalent_305_days_milk_production(self) -> None: - if self.days_in_milk < 305: - self.mature_equivalent_milking_prediction_305_day = self.milk_production.calculate_305_day_milk_yield( - self.milk_production.wood_l, - self.milk_production.wood_m, - self.milk_production.wood_n, - self.milk_production.milk_production_history, - self.days_in_milk, - ) - else: - self.mature_equivalent_milking_prediction_305_day = ( - self.milk_production.current_lactation_305_day_milk_produced - ) - - parity_factor = {1: 1.25, 2: 1.18}.get(self.calves, 1.0) - - self.mature_equivalent_milking_prediction_305_day = ( - self.mature_equivalent_milking_prediction_305_day * parity_factor - ) From 1ec06fc65cd9f9d4ea03a152c3beb05b5819eb19 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 23 Jan 2026 14:10:22 +0000 Subject: [PATCH 049/107] Apply Black Formatting --- RUFAS/biophysical/animal/animal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RUFAS/biophysical/animal/animal.py b/RUFAS/biophysical/animal/animal.py index e69c2bdf78..28c8b772d6 100644 --- a/RUFAS/biophysical/animal/animal.py +++ b/RUFAS/biophysical/animal/animal.py @@ -1555,8 +1555,8 @@ def daily_routines(self, time: RufasTime) -> DailyRoutinesOutput: newborn_calf_config, daily_routines_output.herd_reproduction_statistics = self.daily_reproduction_update(time) - (daily_routines_output.animal_status, daily_routines_output.newborn_calf_config) = ( - self.animal_life_stage_update(time) + daily_routines_output.animal_status, daily_routines_output.newborn_calf_config = self.animal_life_stage_update( + time ) if self.animal_type.is_cow and newborn_calf_config is not None: From 6531f08caa1fbcf2819afddac06f54ec08f3fd4b Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Fri, 23 Jan 2026 14:13:33 +0000 Subject: [PATCH 050/107] Update badges on README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a0a0b44606..39077b8f59 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![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-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-1685%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-1692%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems **RuFaS** is an open-source, next-generation, whole-farm modeling environment that simulates dairy farm production and environmental impact. It is designed to support research, innovation, and sustainable decision-making in ruminant animal agriculture. From 1dcd0adc92e39c7eab690fe0ba8da7ea98434735 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Fri, 23 Jan 2026 23:16:01 +0900 Subject: [PATCH 051/107] Removed ME305 --- RUFAS/biophysical/animal/animal.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/RUFAS/biophysical/animal/animal.py b/RUFAS/biophysical/animal/animal.py index e69c2bdf78..12f8618c0d 100644 --- a/RUFAS/biophysical/animal/animal.py +++ b/RUFAS/biophysical/animal/animal.py @@ -14,7 +14,11 @@ from RUFAS.biophysical.animal.data_types.daily_routines_output import DailyRoutinesOutput from RUFAS.biophysical.animal.data_types.digestive_system import DigestiveSystemInputs from RUFAS.biophysical.animal.data_types.growth import GrowthInputs, GrowthOutputs -from RUFAS.biophysical.animal.data_types.milk_production import MilkProductionInputs, MilkProductionOutputs +from RUFAS.biophysical.animal.data_types.milk_production import ( + MilkProductionInputs, + MilkProductionOutputs, + MilkProductionStatistics, +) from RUFAS.biophysical.animal.data_types.nutrients import NutrientsInputs from RUFAS.biophysical.animal.data_types.nutrition_data_structures import NutritionRequirements, NutritionSupply from RUFAS.biophysical.animal.data_types.pen_history import PenHistory @@ -1073,6 +1077,22 @@ def dead(self) -> bool: """ return True if (self.dead_at_day is not None and self.dead_at_day >= 0) else False + @property + def milk_statistics(self) -> MilkProductionStatistics: + """Returns the milk statistics for the animal.""" + if not self.animal_type.is_cow: + raise TypeError() + return MilkProductionStatistics( + cow_id=self.id, + pen_id=self.pen_history[-1]["pen"], + days_in_milk=self.days_in_milk, + estimated_daily_milk_produced=self.milk_production.daily_milk_produced, + milk_protein=self.milk_production.true_protein_content, + milk_fat=self.milk_production.fat_content, + milk_lactose=self.milk_production.lactose_content, + parity=self.calves, + ) + def _assign_sex_to_newborn_calf(self) -> None: """ Assign a sex to a newborn calf based on the semen type and male calf rate. @@ -1295,7 +1315,7 @@ def _daily_nutrients_update(self) -> None: ) self.nutrients.perform_daily_phosphorus_update(nutrients_inputs) - def _daily_digestive_system_update(self) -> dict[AnimalType, dict[str, float]]: + def _daily_digestive_system_update(self) -> None: """ Performs the daily digestive system updates for the animal. @@ -1320,8 +1340,7 @@ def _daily_digestive_system_update(self) -> dict[AnimalType, dict[str, float]]: fat_content=MilkProduction.fat_percent, protein_content=self.milk_production.true_protein_content, ) - digestion_output = self.digestive_system.process_digestion(digestive_system_inputs) - return digestion_output + self.digestive_system.process_digestion(digestive_system_inputs) def daily_milking_update(self, time: RufasTime) -> None: """ @@ -1546,8 +1565,7 @@ def daily_routines(self, time: RufasTime) -> DailyRoutinesOutput: self._daily_nutrients_update() - digestion_outputs = self._daily_digestive_system_update() - daily_routines_output.daily_digestion_output = digestion_outputs + self._daily_digestive_system_update() self.daily_milking_update(time) From e3fd934e410a8848c86bfb7050ce7e8148c84c73 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 23 Jan 2026 14:17:47 +0000 Subject: [PATCH 052/107] Apply Black Formatting From 1ea548101798099abd07bfbfb2f903a5ee038580 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Fri, 23 Jan 2026 23:19:45 +0900 Subject: [PATCH 053/107] Removed ME305 --- RUFAS/simulation_engine.py | 1 - .../test_animal/test_milk_production/test_milk_production.py | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/RUFAS/simulation_engine.py b/RUFAS/simulation_engine.py index b86471a52f..92f09370ba 100644 --- a/RUFAS/simulation_engine.py +++ b/RUFAS/simulation_engine.py @@ -127,7 +127,6 @@ def _daily_simulation(self) -> None: is_time_to_reformulate_ration = self.time.current_date.date() == self.next_ration_reformulation if is_time_to_reformulate_ration: self._formulate_ration() - # self.herd_manager.update_milk_305_day_yield_predictions() requested_feed = self.herd_manager.collect_daily_feed_request() self.feed_manager.report_feed_storage_levels(self.time.simulation_day, "daily_storage_levels") diff --git a/tests/test_biophysical/test_animal/test_milk_production/test_milk_production.py b/tests/test_biophysical/test_animal/test_milk_production/test_milk_production.py index ff15fd2081..5b400ef12f 100644 --- a/tests/test_biophysical/test_animal/test_milk_production/test_milk_production.py +++ b/tests/test_biophysical/test_animal/test_milk_production/test_milk_production.py @@ -360,9 +360,7 @@ def test_calculate_daily_milk_production( ) def test_calc_305_day_milk_yield(l_param: float, m_param: float, n_param: float, expected: float) -> None: """Test the calc_305_day_milk_yield method of the MilkProduction class.""" - assert MilkProduction.calculate_305_day_milk_yield(l_param, m_param, n_param, None) == pytest.approx( - expected, rel=1e-6 - ) + assert MilkProduction.calc_305_day_milk_yield(l_param, m_param, n_param) == pytest.approx(expected, rel=1e-6) @pytest.mark.parametrize( From 0642c6b3d68fbf3d1248050407c72bc080238ca5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 23 Jan 2026 14:21:31 +0000 Subject: [PATCH 054/107] Apply Black Formatting From 68de240c125f68851e5da59eb6c87364abb2f79c Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Fri, 23 Jan 2026 14:25:06 +0000 Subject: [PATCH 055/107] Update badges on README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 39077b8f59..a0a0b44606 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![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-1692%20errors-red)](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-1685%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems **RuFaS** is an open-source, next-generation, whole-farm modeling environment that simulates dairy farm production and environmental impact. It is designed to support research, innovation, and sustainable decision-making in ruminant animal agriculture. From 80f8e12c0aed3101d7e763921caca8b229d1286f Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Sat, 24 Jan 2026 16:31:35 +0900 Subject: [PATCH 056/107] Fixed flake8 --- .../test_herd_manager/test_herd_manager_daily_routines.py | 8 -------- 1 file changed, 8 deletions(-) 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 38329d7b3b..e1c9bb16b1 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 @@ -380,14 +380,6 @@ def test_daily_routines(herd_manager: HerdManager, mock_herd: dict[str, list[Ani sold_oversupply_heiferIIIs = [mock_animal(AnimalType.HEIFER_III, sold=True) for _ in range(5)] bought_replacement_heiferIIIs = [mock_animal(AnimalType.HEIFER_III, sold=False) for _ in range(5)] - graduated_animals = ( - graduated_calves + graduated_heiferIs + graduated_heiferIIs + graduated_heiferIIIs + graduated_cows - ) - newborn_calves = heiferIII_newborn_calves + cow_newborn_calves - removed_animals = ( - sold_calves + sold_heiferIs + sold_heiferIIs + sold_heiferIIIs + sold_and_died_cows + sold_oversupply_heiferIIIs - ) - mock_perform_daily_routines_for_animals_side_effect: list[ tuple[list[Animal], list[Animal], list[Animal], list[Animal], list[Animal]] ] = [ From 36812d0b6131fdd427c906479609715f5d56a72d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 24 Jan 2026 08:00:16 +0000 Subject: [PATCH 057/107] Apply Black Formatting From 064e23be4dc5e2deda0a31ab92977f2c058a4595 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Sat, 24 Jan 2026 08:03:46 +0000 Subject: [PATCH 058/107] Update badges on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a0a0b44606..beb060546d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Flake8](https://img.shields.io/badge/Flake8-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![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-1685%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) From 22f2264fc2c7a5ccf617b0478edc63a1586fad07 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Thu, 29 Jan 2026 01:49:39 +0900 Subject: [PATCH 059/107] Fixed herd exit nums --- RUFAS/biophysical/animal/herd_manager.py | 2 +- .../test_herd_manager/test_herd_manager_daily_routines.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 4ce5db9be5..fafb4752c9 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -732,7 +732,7 @@ def _record_sold_cow_stats(self, removed_cow: Animal, simulation_day: int) -> No ) self.herd_statistics.cow_num -= 1 self.herd_statistics.sold_cow_oversupply_num += 1 - self.herd_statistics.cow_herd_exit_num -= 1 + self.herd_statistics.cow_herd_exit_num += 1 self.herd_statistics.sold_cow_num += 1 def _check_if_cows_need_to_be_sold(self, simulation_day: int, removed_animal: list[Animal]) -> list[Animal]: 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 e1c9bb16b1..ab70c3f920 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 @@ -568,7 +568,7 @@ def test_check_if_cows_need_to_be_sold_comprehensive(herd_manager: HerdManager, assert herd_manager.herd_statistics.sold_cow_oversupply_num == 5 assert herd_manager.herd_statistics.sold_cow_num == 5 - assert herd_manager.herd_statistics.cow_herd_exit_num == 5 + assert herd_manager.herd_statistics.cow_herd_exit_num == 15 assert len(herd_manager.herd_statistics.sold_cows_info) == 5 assert herd_manager.herd_statistics.sold_cows_info[0]["id"] == cow_dnb_low_milk.id From d772b1bf042d208d3ee8d2f72c94c52ac514f913 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 28 Jan 2026 16:51:21 +0000 Subject: [PATCH 060/107] Apply Black Formatting From e78a58d2a3e14b0432dff49d7d018910f328465b Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Thu, 29 Jan 2026 03:55:25 +0900 Subject: [PATCH 061/107] Removed old logic --- RUFAS/biophysical/animal/animal.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/RUFAS/biophysical/animal/animal.py b/RUFAS/biophysical/animal/animal.py index efb1b2a278..6f7e08e03e 100644 --- a/RUFAS/biophysical/animal/animal.py +++ b/RUFAS/biophysical/animal/animal.py @@ -1748,14 +1748,6 @@ def animal_life_stage_update(self, time: RufasTime) -> tuple[AnimalStatus, NewBo self.cull_reason = animal_constants.DEATH_CULL animal_status = AnimalStatus.DEAD - if ( - self.animal_type.is_cow - and self.reproduction.do_not_breed - and self.milk_production.daily_milk_produced < AnimalConfig.cull_milk_production - ): - self.cull_reason = animal_constants.LOW_PROD_CULL - self.sold_at_day = time.simulation_day - animal_status = AnimalStatus.SOLD return animal_status, newborn_calf_config def _evaluate_calf_for_heiferI(self) -> bool: From 7167ec5bac6a87ae8805e3f8cdae70156f3d65bd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 28 Jan 2026 18:57:10 +0000 Subject: [PATCH 062/107] Apply Black Formatting From 919c331d0fae0308d0523773204c59adc542d02c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 25 Feb 2026 13:59:10 +0000 Subject: [PATCH 063/107] Apply Black Formatting From e4892d1a59b860228ca3c89b0fdd98d2ec5355ec Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Wed, 25 Feb 2026 14:02:46 +0000 Subject: [PATCH 064/107] Update badges on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bd8e07077f..ec7eb304ad 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-1685%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1686%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems From 1d15c54a8e5dd644b1b7d8d3e34c6e0309772cf3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 26 Feb 2026 14:38:12 +0000 Subject: [PATCH 065/107] Apply Black Formatting From fdff74add81821748dd6918b9e11df40492ec662 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Thu, 26 Feb 2026 23:40:37 +0900 Subject: [PATCH 066/107] Input changes --- RUFAS/biophysical/animal/herd_manager.py | 4 ++-- input/data/animal/example_freestall_animal.json | 5 ++--- input/data/animal/example_open_lot_animal.json | 5 ++--- input/metadata/properties/default.json | 16 +++++----------- 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 925362117e..5eea882992 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -126,8 +126,8 @@ def __init__( self.herd_statistics = HerdStatistics() self.herd_statistics.herd_num = animal_config_data["herd_information"]["herd_num"] self.adjustment_period = animal_config_data["herd_information"]["herd_size_adjustment_period"] - self.selling_threshold = animal_config_data["herd_information"]["herd_selling_threshold"] - self.buying_threshold = animal_config_data["herd_information"]["herd_buying_threshold"] + self.selling_threshold = animal_config_data["herd_information"]["herd_size_sell_threshold"] / 100 + self.buying_threshold = animal_config_data["herd_information"]["herd_size_buy_threshold"] / 100 self.herd_reproduction_statistics = HerdReproductionStatistics() self.housing = animal_config_data["housing"] diff --git a/input/data/animal/example_freestall_animal.json b/input/data/animal/example_freestall_animal.json index bf703b7752..752d241a68 100644 --- a/input/data/animal/example_freestall_animal.json +++ b/input/data/animal/example_freestall_animal.json @@ -8,8 +8,8 @@ "replace_num": 500, "herd_num": 100, "herd_size_adjustment_period": 30, - "herd_selling_threshold": 1.03, - "herd_buying_threshold": 1.01, + "herd_size_sell_threshold": 103, + "herd_size_buy_threshold": 101, "breed": "HO", "parity_fractions": { "1": 0.36, @@ -34,7 +34,6 @@ "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 diff --git a/input/data/animal/example_open_lot_animal.json b/input/data/animal/example_open_lot_animal.json index c86e6a2a88..1bd8904207 100644 --- a/input/data/animal/example_open_lot_animal.json +++ b/input/data/animal/example_open_lot_animal.json @@ -8,8 +8,8 @@ "replace_num": 5000, "herd_num": 1000, "herd_size_adjustment_period": 30, - "herd_selling_threshold": 1.03, - "herd_buying_threshold": 1.01, + "herd_size_sell_threshold": 103, + "herd_size_buy_threshold": 101, "breed": "HO", "parity_fractions": { "1": 0.35, @@ -34,7 +34,6 @@ "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 diff --git a/input/metadata/properties/default.json b/input/metadata/properties/default.json index 793a9988a0..072c0f2ab8 100644 --- a/input/metadata/properties/default.json +++ b/input/metadata/properties/default.json @@ -89,17 +89,17 @@ "default": 30, "minimum": 1 }, - "herd_selling_threshold": { + "herd_size_sell_threshold": { "type": "number", "description": "The threshold that should sell animals to maintain herd size.", - "default": 1.03, + "default": 101, "minimum": 1 }, - "herd_buying_threshold": { + "herd_size_buy_threshold": { "type": "number", "description": "The threshold that should buy animals to maintain herd size.", - "default": 0.98, - "minimum": 0.0 + "default": 95, + "minimum": 0 }, "breed": { "type": "string", @@ -220,12 +220,6 @@ "default": 185, "minimum": 0 }, - "cull_milk_production": { - "type": "number", - "description": "Cull Milk Production (kg/d) -- Milk production threshold at which 'do not breed' cows are culled if they fall below", - "default": 30, - "minimum": 0 - }, "cow_times_milked_per_day": { "type": "number", "description": "Number of Milkings (per day) -- The average or most common number of times cows are milked per day (1, 2, or 3 times daily)", From cf4831f394f72b472c553d0d08d67e034e658be5 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Fri, 27 Feb 2026 18:56:18 +0900 Subject: [PATCH 067/107] All changes --- RUFAS/biophysical/animal/herd_manager.py | 21 +++------ .../test_herd_manager/pytest_fixtures.py | 4 +- .../test_herd_manager_daily_routines.py | 46 +++++++++++-------- .../test_herd_manager_initialization.py | 4 +- 4 files changed, 37 insertions(+), 38 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 5eea882992..c44e056499 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -694,28 +694,21 @@ def _create_newborn_calf(self, newborn_calf_config: NewBornCalfValuesTypedDict, return newborn_calf def _get_cow_removal_index(self, removed_animal: list[Animal]) -> int | None: - """Finds the index of the best candidate cow to sell based on priority rules.""" + """Finds the index of the eligible cow with the lowest daily milk production.""" MIN_DIM_FOR_REMOVAL = 60 - dnb_indices = [] - non_dnb_indices = [] + MAX_DAYS_IN_PREG_FOR_REMOVAL = 180 + eligible_indices = [] for index, cow in enumerate(self.cows): if cow in removed_animal: continue - if cow.reproduction.do_not_breed: - dnb_indices.append(index) - elif cow.days_in_milk > MIN_DIM_FOR_REMOVAL: - non_dnb_indices.append(index) + if cow.days_in_milk > MIN_DIM_FOR_REMOVAL and cow.days_in_pregnancy < MAX_DAYS_IN_PREG_FOR_REMOVAL: + eligible_indices.append(index) - if not dnb_indices and not non_dnb_indices: + if not eligible_indices: return None - # Priority 1: DNB cows by lowest daily milk - if dnb_indices: - return min(dnb_indices, key=lambda i: self.cows[i].milk_production.daily_milk_produced) - - # Priority 2: Non-DNB cows by lowest daily milk (qualified by DIM) - return min(non_dnb_indices, key=lambda i: self.cows[i].milk_production.daily_milk_produced) + return min(eligible_indices, key=lambda i: self.cows[i].milk_production.daily_milk_produced) def _record_sold_cow_stats(self, removed_cow: Animal, simulation_day: int) -> None: """Updates herd statistics and metadata for a sold cow.""" 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 1aac7d88c4..fa1c0366b6 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 @@ -44,8 +44,8 @@ def animal_json() -> dict[str, Any]: return { "herd_information": { "herd_size_adjustment_period": 30, - "herd_selling_threshold": 1.03, - "herd_buying_threshold": 1.01, + "herd_size_sell_threshold": 103, + "herd_size_buy_threshold": 101, "calf_num": 8, "heiferI_num": 44, "heiferII_num": 38, 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 ab70c3f920..965762a894 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 @@ -488,7 +488,9 @@ def test_create_newborn_calf( animal.events.add_event.assert_called_once() -def _create_sortable_mock_cow(id_val: int, is_dnb: bool, daily_milk: float, days_in_milk: int) -> MagicMock: +def _create_sortable_mock_cow( + id_val: int, is_dnb: bool, daily_milk: float, days_in_milk: int, days_in_pregnancy: int +) -> MagicMock: """Helper to create a mock cow with specific sorting attributes.""" cow = MagicMock(spec=Animal) cow.id = id_val @@ -504,6 +506,7 @@ def _create_sortable_mock_cow(id_val: int, is_dnb: bool, daily_milk: float, days cow.milk_production.daily_milk_produced = daily_milk cow.days_in_milk = days_in_milk + cow.days_in_pregnancy = days_in_pregnancy return cow @@ -512,9 +515,9 @@ def test_check_if_cows_need_to_be_sold_comprehensive(herd_manager: HerdManager, Unit test for _check_if_cows_need_to_be_sold(). Verifies: - 1. Cows marked 'do_not_breed' (DNB) are removed first. - 2. Within priority groups, cows are removed by lowest milk production. - 3. Non-DNB cows with DIM < 60 are protected (skipped). + 1. All eligible cows are ranked only by lowest milk production. + 2. Cows with DIM <= 60 are protected (skipped). + 3. Cows with days in pregnancy >= 180 are protected (skipped). 4. Error is logged if herd is too large but no cows are eligible. 5. Statistics are updated correctly based on source code logic. """ @@ -529,15 +532,16 @@ def test_check_if_cows_need_to_be_sold_comprehensive(herd_manager: HerdManager, herd_manager.herd_statistics.sold_cow_num = 0 herd_manager.herd_statistics.cow_herd_exit_num = 10 - cow_dnb_low_milk = _create_sortable_mock_cow(1, True, 10.0, 100) - cow_dnb_high_milk = _create_sortable_mock_cow(2, True, 50.0, 100) - cow_normal_low_milk = _create_sortable_mock_cow(3, False, 20.0, 100) - cow_normal_high_milk = _create_sortable_mock_cow(4, False, 40.0, 100) - cow_protected_low_dim = _create_sortable_mock_cow(5, False, 5.0, 10) + cow_dnb_low_milk = _create_sortable_mock_cow(1, True, 10.0, 100, 50) + cow_dnb_high_milk = _create_sortable_mock_cow(2, True, 50.0, 100, 60) + cow_normal_low_milk = _create_sortable_mock_cow(3, False, 20.0, 100, 70) + cow_normal_high_milk = _create_sortable_mock_cow(4, False, 40.0, 100, 80) + cow_protected_low_dim = _create_sortable_mock_cow(5, False, 5.0, 10, 0) + cow_protected_high_preg = _create_sortable_mock_cow(6, False, 1.0, 200, 180) fillers = [] for i in range(10): - fillers.append(_create_sortable_mock_cow(10 + i, False, 100.0, 200)) + fillers.append(_create_sortable_mock_cow(10 + i, False, 100.0, 200, 20)) fillers[0].milk_production.daily_milk_produced = 90.0 @@ -547,6 +551,7 @@ def test_check_if_cows_need_to_be_sold_comprehensive(herd_manager: HerdManager, cow_normal_low_milk, cow_normal_high_milk, cow_protected_low_dim, + cow_protected_high_preg, ] + fillers herd_manager.cows = all_cows @@ -555,30 +560,31 @@ def test_check_if_cows_need_to_be_sold_comprehensive(herd_manager: HerdManager, removed_cows = herd_manager._check_if_cows_need_to_be_sold(SIMULATION_DAY, []) - assert len(removed_cows) == 5 + assert len(removed_cows) == 6 assert len(herd_manager.cows) == 10 assert removed_cows[0] == cow_dnb_low_milk - assert removed_cows[1] == cow_dnb_high_milk - assert removed_cows[2] == cow_normal_low_milk - assert removed_cows[3] == cow_normal_high_milk + assert removed_cows[1] == cow_normal_low_milk + assert removed_cows[2] == cow_normal_high_milk + assert removed_cows[3] == cow_dnb_high_milk assert removed_cows[4] == fillers[0] assert cow_protected_low_dim in herd_manager.cows + assert cow_protected_high_preg in herd_manager.cows - assert herd_manager.herd_statistics.sold_cow_oversupply_num == 5 - assert herd_manager.herd_statistics.sold_cow_num == 5 - assert herd_manager.herd_statistics.cow_herd_exit_num == 15 + assert herd_manager.herd_statistics.sold_cow_oversupply_num == 6 + assert herd_manager.herd_statistics.sold_cow_num == 6 + assert herd_manager.herd_statistics.cow_herd_exit_num == 16 - assert len(herd_manager.herd_statistics.sold_cows_info) == 5 + assert len(herd_manager.herd_statistics.sold_cows_info) == 6 assert herd_manager.herd_statistics.sold_cows_info[0]["id"] == cow_dnb_low_milk.id assert herd_manager.herd_statistics.sold_cows_info[0]["sold_at_day"] == SIMULATION_DAY herd_manager.herd_statistics.herd_num = 1 herd_manager.selling_threshold = 1.0 - cow_prot_1 = _create_sortable_mock_cow(99, False, 100.0, 10) - cow_prot_2 = _create_sortable_mock_cow(98, False, 100.0, 10) + cow_prot_1 = _create_sortable_mock_cow(99, False, 100.0, 10, 0) + cow_prot_2 = _create_sortable_mock_cow(98, False, 100.0, 10, 0) herd_manager.cows = [cow_prot_1, cow_prot_2] stuck_result = herd_manager._check_if_cows_need_to_be_sold(SIMULATION_DAY, []) diff --git a/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_initialization.py b/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_initialization.py index c1df92c3f2..219fb1efbb 100644 --- a/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_initialization.py +++ b/tests/test_biophysical/test_animal/test_herd_manager/test_herd_manager_initialization.py @@ -103,8 +103,8 @@ def test_init_uses_set_ration_feeds_when_not_user_defined(mocker: MockerFixture) "herd_information": { "herd_num": 123, "herd_size_adjustment_period": 30, - "herd_selling_threshold": 1.01, - "herd_buying_threshold": 1.06, + "herd_size_sell_threshold": 101, + "herd_size_buy_threshold": 106, }, "housing": "barn", "pasture_concentrate": 0, From e45bab9f788453c7d5e101c879f018bf9dc4c9ed Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 27 Feb 2026 09:58:15 +0000 Subject: [PATCH 068/107] Apply Black Formatting From 64633df347df1d377f3fce3371921811e19c30a6 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Tue, 3 Mar 2026 01:45:17 +0900 Subject: [PATCH 069/107] All changes --- RUFAS/biophysical/animal/animal_config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/RUFAS/biophysical/animal/animal_config.py b/RUFAS/biophysical/animal/animal_config.py index 4f73adecdf..553c75a0fc 100644 --- a/RUFAS/biophysical/animal/animal_config.py +++ b/RUFAS/biophysical/animal/animal_config.py @@ -193,7 +193,6 @@ class AnimalConfig: dry_off_day_of_pregnancy: int = 218 heifer_reproduction_cull_day: int = 500 do_not_breed_time: int = 185 - cull_milk_production: int = 30 semen_type: str = "conventional" male_calf_rate_conventional_semen: float = 0.53 @@ -392,7 +391,6 @@ def initialize_animal_config(cls) -> None: cls.dry_off_day_of_pregnancy = animal_config_data["management_decisions"]["days_in_preg_when_dry"] cls.heifer_reproduction_cull_day = animal_config_data["management_decisions"]["heifer_repro_cull_time"] cls.do_not_breed_time = animal_config_data["management_decisions"]["do_not_breed_time"] - 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"][ From 1fb04eba72261aa5ebd988505ebd7dda488d07db Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Tue, 3 Mar 2026 01:46:50 +0900 Subject: [PATCH 070/107] Reducing mypy --- RUFAS/biophysical/field/soil/infiltration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RUFAS/biophysical/field/soil/infiltration.py b/RUFAS/biophysical/field/soil/infiltration.py index 443d69596d..d6fc49dc5e 100644 --- a/RUFAS/biophysical/field/soil/infiltration.py +++ b/RUFAS/biophysical/field/soil/infiltration.py @@ -101,7 +101,7 @@ def infiltrate(self, rainfall: float) -> None: # --- static methods --- @staticmethod - def _determine_first_moisture_condition_parameter(second_moisture_condition: float): + def _determine_first_moisture_condition_parameter(second_moisture_condition: float) -> float: """ Determine the curve number for dry (wilting point) conditions. @@ -125,7 +125,7 @@ def _determine_first_moisture_condition_parameter(second_moisture_condition: flo return second_moisture_condition - (top / bottom) @staticmethod - def _determine_third_moisture_condition_parameter(second_moisture_condition: float): + def _determine_third_moisture_condition_parameter(second_moisture_condition: float) -> float: """ Determine the curve number for wet (field capacity) conditions. From c2d8100b3e8d2a27ce216940a085f8ea12248111 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 2 Mar 2026 16:48:39 +0000 Subject: [PATCH 071/107] Apply Black Formatting From 1b1158c84fa8278e2c964349f45ec68cc8fb5546 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Mon, 2 Mar 2026 16:52:19 +0000 Subject: [PATCH 072/107] Update badges on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ec7eb304ad..b85a1a5a21 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-1686%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1684%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems From 5686041233a839cc013bb9a07905c37a0de40662 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Mar 2026 07:18:04 +0000 Subject: [PATCH 073/107] Apply Black Formatting From 43e11e1a5f1c980864eeccef7bfd00fb80581319 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 9 Mar 2026 07:37:50 +0000 Subject: [PATCH 074/107] Apply Black Formatting From 4002e3de482fc2d7731922d303d46accc2372a7f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 9 Mar 2026 14:49:21 +0000 Subject: [PATCH 075/107] Apply Black Formatting From 273e86d23997f5efa4e018ab770ef303bc26c43a Mon Sep 17 00:00:00 2001 From: Matthew Liu <49083256+matthew7838@users.noreply.github.com> Date: Tue, 10 Mar 2026 23:01:45 +0900 Subject: [PATCH 076/107] Update input/metadata/properties/default.json Co-authored-by: Niko <70217952+ew3361zh@users.noreply.github.com> --- input/metadata/properties/default.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/input/metadata/properties/default.json b/input/metadata/properties/default.json index fea99b9b3d..2d8ff6cc50 100644 --- a/input/metadata/properties/default.json +++ b/input/metadata/properties/default.json @@ -85,7 +85,7 @@ }, "herd_size_adjustment_period": { "type": "number", - "description": "The period between each check to adjust the her's size.", + "description": "The period between each check to adjust the herd's size.", "default": 30, "minimum": 1 }, From 5975eb87624c497b355f28e0538c7abd4deb9afd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 10 Mar 2026 14:05:36 +0000 Subject: [PATCH 077/107] Apply Black Formatting From 3f353f086797904755cf6d2c1dd72389278831eb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Mar 2026 07:35:46 +0000 Subject: [PATCH 078/107] Apply Black Formatting From d0a9cdfd2010307770c13435464781a3a3a6955c Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Wed, 11 Mar 2026 17:09:52 +0900 Subject: [PATCH 079/107] Addressed Niko's comments --- RUFAS/biophysical/animal/animal_constants.py | 6 ++++++ RUFAS/biophysical/animal/herd_manager.py | 20 +++++++++----------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/RUFAS/biophysical/animal/animal_constants.py b/RUFAS/biophysical/animal/animal_constants.py index cef364e522..349e194b99 100644 --- a/RUFAS/biophysical/animal/animal_constants.py +++ b/RUFAS/biophysical/animal/animal_constants.py @@ -44,6 +44,12 @@ # heifer repro INJECT_CIDR = "inject CIDR" +# herd size management +MIN_DIM_FOR_REMOVAL = 60 +"""Minimum days in milk required for a cow to be eligible for removal.""" +MAX_DAYS_IN_PREG_FOR_REMOVAL = 180 +"""Maximum pregnancy duration for a cow to be eligible for removal.""" + # presynch protocols PRESYNCH_PERIOD_START = "Presynch period started" PRESYNCH_PERIOD_END = "Presynch period ended" diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index e883ce15e7..7dbef33525 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -597,7 +597,9 @@ def daily_routines( ) self._update_stillborn_calf_statistics(stillborn_newborn_calves) - if time.simulation_day > 0 and time.simulation_day % self.adjustment_period == 0: + + adjust_herd_size: bool = time.simulation_day > 0 and time.simulation_day % self.adjustment_period == 0 + if adjust_herd_size: removed_animals += self._check_if_cows_need_to_be_sold( simulation_day=time.simulation_day, removed_animal=removed_animals ) @@ -668,8 +670,7 @@ def _report_ration(self, simulation_day: int) -> None: def _create_newborn_calf(self, newborn_calf_config: NewBornCalfValuesTypedDict, simulation_day: int) -> Animal: """ - Creates a new newborn calf instance and records its entry event in the herd if it - is not sold. + Creates a new newborn calf instance and records its entry event in the herd if it is not sold. Parameters ---------- @@ -691,15 +692,16 @@ def _create_newborn_calf(self, newborn_calf_config: NewBornCalfValuesTypedDict, return newborn_calf def _get_cow_removal_index(self, removed_animal: list[Animal]) -> int | None: - """Finds the index of the eligible cow with the lowest daily milk production.""" - MIN_DIM_FOR_REMOVAL = 60 - MAX_DAYS_IN_PREG_FOR_REMOVAL = 180 + """Finds the indices of cows with the lowest daily milk production among cows that meet the specified + days-in-milk and days-pregnant criteria.""" eligible_indices = [] for index, cow in enumerate(self.cows): if cow in removed_animal: continue - if cow.days_in_milk > MIN_DIM_FOR_REMOVAL and cow.days_in_pregnancy < MAX_DAYS_IN_PREG_FOR_REMOVAL: + eligible_for_removal = (cow.days_in_milk > animal_constants.MIN_DIM_FOR_REMOVAL + and cow.days_in_pregnancy < animal_constants.MAX_DAYS_IN_PREG_FOR_REMOVAL) + if eligible_for_removal: eligible_indices.append(index) if not eligible_indices: @@ -2062,7 +2064,3 @@ def _update_total_enteric_methane(self, digestive_outputs: list[dict[AnimalType, self.herd_statistics.total_enteric_methane[animal_type] = { k: float(current_totals.get(k, 0) + new_emissions.get(k, 0)) for k in all_keys } - - def update_milk_305_day_yield_predictions(self) -> None: - for cow in self.cows: - cow.update_mature_equivalent_305_days_milk_production() From d89a1427e8e870643278bcdf61a44ac4dc52f940 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Mar 2026 08:11:39 +0000 Subject: [PATCH 080/107] Apply Black Formatting --- RUFAS/biophysical/animal/herd_manager.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 7dbef33525..a26e9a6dc6 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -693,14 +693,16 @@ def _create_newborn_calf(self, newborn_calf_config: NewBornCalfValuesTypedDict, def _get_cow_removal_index(self, removed_animal: list[Animal]) -> int | None: """Finds the indices of cows with the lowest daily milk production among cows that meet the specified - days-in-milk and days-pregnant criteria.""" + days-in-milk and days-pregnant criteria.""" eligible_indices = [] for index, cow in enumerate(self.cows): if cow in removed_animal: continue - eligible_for_removal = (cow.days_in_milk > animal_constants.MIN_DIM_FOR_REMOVAL - and cow.days_in_pregnancy < animal_constants.MAX_DAYS_IN_PREG_FOR_REMOVAL) + eligible_for_removal = ( + cow.days_in_milk > animal_constants.MIN_DIM_FOR_REMOVAL + and cow.days_in_pregnancy < animal_constants.MAX_DAYS_IN_PREG_FOR_REMOVAL + ) if eligible_for_removal: eligible_indices.append(index) From beda934337d2e9f806f28ae2dbc7d58a5255ef62 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Wed, 11 Mar 2026 08:15:23 +0000 Subject: [PATCH 081/107] Update badges on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 366064074f..c206e8495b 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-1249%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1248%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems From 601dd648f561654b6b93665f87d1e5c8b48935f5 Mon Sep 17 00:00:00 2001 From: Matthew Liu <49083256+matthew7838@users.noreply.github.com> Date: Wed, 11 Mar 2026 23:22:48 +0900 Subject: [PATCH 082/107] Update input/data/animal/example_freestall_animal.json Co-authored-by: jadamchick <146877580+jadamchick@users.noreply.github.com> --- input/data/animal/example_freestall_animal.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/input/data/animal/example_freestall_animal.json b/input/data/animal/example_freestall_animal.json index 752d241a68..d652547b91 100644 --- a/input/data/animal/example_freestall_animal.json +++ b/input/data/animal/example_freestall_animal.json @@ -8,7 +8,7 @@ "replace_num": 500, "herd_num": 100, "herd_size_adjustment_period": 30, - "herd_size_sell_threshold": 103, + "herd_size_sell_threshold": 101, "herd_size_buy_threshold": 101, "breed": "HO", "parity_fractions": { From b33d8f64870f41520ec9d887282b43e0bbf0732b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Mar 2026 14:24:33 +0000 Subject: [PATCH 083/107] Apply Black Formatting From 2007bd23afa3e2bcfa317f7b7ecbb8859a99ed3d Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Wed, 11 Mar 2026 23:49:08 +0900 Subject: [PATCH 084/107] Julie's and part of Allister's comments --- RUFAS/biophysical/animal/animal_config.py | 2 -- RUFAS/biophysical/animal/herd_manager.py | 13 ++++++------- input/data/animal/example_freestall_animal.json | 2 +- input/data/animal/example_open_lot_animal.json | 4 ++-- input/metadata/properties/default.json | 2 -- .../test_herd_manager_daily_routines.py | 2 +- 6 files changed, 10 insertions(+), 15 deletions(-) diff --git a/RUFAS/biophysical/animal/animal_config.py b/RUFAS/biophysical/animal/animal_config.py index 553c75a0fc..ac749df44b 100644 --- a/RUFAS/biophysical/animal/animal_config.py +++ b/RUFAS/biophysical/animal/animal_config.py @@ -37,8 +37,6 @@ class AnimalConfig: Maximum day at which a heifer is culled if not pregnant, (simulation days). do_not_breed_time : int The duration after which breeding is stopped, (simulation days). - cull_milk_production : int - The threshold milk production below which cows are culled, (simulation days). semen_type : str Types of semen used for reproduction, e.g., "conventional", (unitless). male_calf_rate_conventional_semen : float diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index a26e9a6dc6..4cb290a263 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -126,8 +126,8 @@ def __init__( self.herd_statistics = HerdStatistics() self.herd_statistics.herd_num = animal_config_data["herd_information"]["herd_num"] self.adjustment_period = animal_config_data["herd_information"]["herd_size_adjustment_period"] - self.selling_threshold = animal_config_data["herd_information"]["herd_size_sell_threshold"] / 100 - self.buying_threshold = animal_config_data["herd_information"]["herd_size_buy_threshold"] / 100 + self.selling_threshold = animal_config_data["herd_information"]["herd_size_sell_threshold"] + self.buying_threshold = animal_config_data["herd_information"]["herd_size_buy_threshold"] self.herd_reproduction_statistics = HerdReproductionStatistics() self.housing = animal_config_data["housing"] @@ -720,9 +720,9 @@ def _record_sold_cow_stats(self, removed_cow: Animal, simulation_day: int) -> No animal_type=removed_cow.animal_type.value, sold_at_day=removed_cow.sold_at_day, body_weight=removed_cow.body_weight, - cull_reason="NA", + cull_reason="Herd resize", days_in_milk=removed_cow.days_in_milk, - parity="NA", + parity=removed_cow.calves, ) ) self.herd_statistics.cow_num -= 1 @@ -734,7 +734,7 @@ def _check_if_cows_need_to_be_sold(self, simulation_day: int, removed_animal: li """Checks if surplus cows need to be sold based on herd size.""" animals_removed: list[Animal] = [] - while len(self.cows) > self.herd_statistics.herd_num * self.selling_threshold and len(self.cows) > 0: + while len(self.cows) > self.selling_threshold and len(self.cows) > 0: remove_index = self._get_cow_removal_index(removed_animal) if remove_index is None: @@ -768,8 +768,7 @@ def _check_if_replacement_heifers_needed(self, time: RufasTime) -> list[Animal]: """ animals_added: list[Animal] = [] while ( - len(self.cows) + self.herd_statistics.bought_heifer_num - < self.herd_statistics.herd_num * self.buying_threshold + len(self.cows) + self.herd_statistics.bought_heifer_num < self.buying_threshold and time.simulation_day > 1 ): if len(self.replacement_market) == 0: diff --git a/input/data/animal/example_freestall_animal.json b/input/data/animal/example_freestall_animal.json index d652547b91..06ad135b05 100644 --- a/input/data/animal/example_freestall_animal.json +++ b/input/data/animal/example_freestall_animal.json @@ -9,7 +9,7 @@ "herd_num": 100, "herd_size_adjustment_period": 30, "herd_size_sell_threshold": 101, - "herd_size_buy_threshold": 101, + "herd_size_buy_threshold": 95, "breed": "HO", "parity_fractions": { "1": 0.36, diff --git a/input/data/animal/example_open_lot_animal.json b/input/data/animal/example_open_lot_animal.json index 1bd8904207..be78c46404 100644 --- a/input/data/animal/example_open_lot_animal.json +++ b/input/data/animal/example_open_lot_animal.json @@ -8,8 +8,8 @@ "replace_num": 5000, "herd_num": 1000, "herd_size_adjustment_period": 30, - "herd_size_sell_threshold": 103, - "herd_size_buy_threshold": 101, + "herd_size_sell_threshold": 1010, + "herd_size_buy_threshold": 980, "breed": "HO", "parity_fractions": { "1": 0.35, diff --git a/input/metadata/properties/default.json b/input/metadata/properties/default.json index 1e15de14b6..8720a15b05 100644 --- a/input/metadata/properties/default.json +++ b/input/metadata/properties/default.json @@ -92,13 +92,11 @@ "herd_size_sell_threshold": { "type": "number", "description": "The threshold that should sell animals to maintain herd size.", - "default": 101, "minimum": 1 }, "herd_size_buy_threshold": { "type": "number", "description": "The threshold that should buy animals to maintain herd size.", - "default": 95, "minimum": 0 }, "breed": { 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 965762a894..ee2f61efa7 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 @@ -522,7 +522,7 @@ def test_check_if_cows_need_to_be_sold_comprehensive(herd_manager: HerdManager, 5. Statistics are updated correctly based on source code logic. """ HERD_TARGET = 10 - SELLING_THRESHOLD = 1.0 + SELLING_THRESHOLD = 10 SIMULATION_DAY = 100 herd_manager.herd_statistics.herd_num = HERD_TARGET From 654b9bd73b768044be5a86eddc32d30017dbf0b4 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Thu, 12 Mar 2026 03:50:47 +0900 Subject: [PATCH 085/107] Addressed all comments --- RUFAS/biophysical/animal/animal_constants.py | 2 +- .../animal/animal_module_reporter.py | 2 +- .../animal/data_types/herd_statistics.py | 4 +- RUFAS/biophysical/animal/herd_manager.py | 40 ++++--------------- .../test_herd_manager_daily_routines.py | 8 ---- .../test_herd_manager_herd_statistics.py | 28 ++++++------- 6 files changed, 25 insertions(+), 59 deletions(-) diff --git a/RUFAS/biophysical/animal/animal_constants.py b/RUFAS/biophysical/animal/animal_constants.py index 349e194b99..b4c9c254cb 100644 --- a/RUFAS/biophysical/animal/animal_constants.py +++ b/RUFAS/biophysical/animal/animal_constants.py @@ -92,7 +92,7 @@ # culling HEIFER_REPRO_CULL = "culled for heifer reproductive problem" -LOW_PROD_CULL = "culled for low production" +OVERSUPPLY_CULL = "culled for herd resize" DEATH_CULL = "culled for death" LAMENESS_CULL = "culled for lameness" INJURY_CULL = "culled for injury" diff --git a/RUFAS/biophysical/animal/animal_module_reporter.py b/RUFAS/biophysical/animal/animal_module_reporter.py index eb10aa994a..5d55cf3152 100644 --- a/RUFAS/biophysical/animal/animal_module_reporter.py +++ b/RUFAS/biophysical/animal/animal_module_reporter.py @@ -840,7 +840,7 @@ def report_herd_statistics_data(cls, herd_statistics: HerdStatistics, simulation ) cull_reason_stats_units = { animal_constants.DEATH_CULL: MeasurementUnits.UNITLESS, - animal_constants.LOW_PROD_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, diff --git a/RUFAS/biophysical/animal/data_types/herd_statistics.py b/RUFAS/biophysical/animal/data_types/herd_statistics.py index 27511e2ed1..ef202b6209 100644 --- a/RUFAS/biophysical/animal/data_types/herd_statistics.py +++ b/RUFAS/biophysical/animal/data_types/herd_statistics.py @@ -256,7 +256,7 @@ def __init__(self) -> None: } self.cull_reason_stats = { animal_constants.DEATH_CULL: 0, - animal_constants.LOW_PROD_CULL: 0, + animal_constants.OVERSUPPLY_CULL: 0, animal_constants.LAMENESS_CULL: 0, animal_constants.INJURY_CULL: 0, animal_constants.MASTITIS_CULL: 0, @@ -270,7 +270,7 @@ def __init__(self) -> None: self.avg_age_for_parity = {"1": 0, "2": 0, "3": 0, "4": 0, "5": 0, "greater_than_5": 0} self.cull_reason_stats_percent = { animal_constants.DEATH_CULL: 0.0, - animal_constants.LOW_PROD_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, diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 4cb290a263..c8d679fc52 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -271,19 +271,6 @@ def phosphorus_concentration_by_animal_class(self) -> dict[AnimalType, float]: return phosphorus_concentration_by_animal_class - @property - def current_herd_size(self) -> int: - """ - Calculates the current size of the herd based on the number of heiferIIIs and cows. - - Returns - ------- - int - The current size of the herd. - - """ - return len(self.heiferIIIs) + len(self.cows) - @property def heiferII_events_by_id(self) -> dict[str, AnimalEvents]: """ @@ -603,6 +590,7 @@ def daily_routines( removed_animals += self._check_if_cows_need_to_be_sold( simulation_day=time.simulation_day, removed_animal=removed_animals ) + self._update_sold_and_died_cow_statistics(removed_animals) newly_added_animals = self._check_if_replacement_heifers_needed(time=time) self._update_herd_structure( @@ -711,25 +699,6 @@ def _get_cow_removal_index(self, removed_animal: list[Animal]) -> int | None: return min(eligible_indices, key=lambda i: self.cows[i].milk_production.daily_milk_produced) - def _record_sold_cow_stats(self, removed_cow: Animal, simulation_day: int) -> None: - """Updates herd statistics and metadata for a sold cow.""" - removed_cow.sold_at_day = simulation_day - self.herd_statistics.sold_cows_info.append( - SoldAnimalTypedDict( - id=removed_cow.id, - animal_type=removed_cow.animal_type.value, - sold_at_day=removed_cow.sold_at_day, - body_weight=removed_cow.body_weight, - cull_reason="Herd resize", - days_in_milk=removed_cow.days_in_milk, - parity=removed_cow.calves, - ) - ) - self.herd_statistics.cow_num -= 1 - self.herd_statistics.sold_cow_oversupply_num += 1 - self.herd_statistics.cow_herd_exit_num += 1 - self.herd_statistics.sold_cow_num += 1 - def _check_if_cows_need_to_be_sold(self, simulation_day: int, removed_animal: list[Animal]) -> list[Animal]: """Checks if surplus cows need to be sold based on herd size.""" animals_removed: list[Animal] = [] @@ -742,7 +711,8 @@ def _check_if_cows_need_to_be_sold(self, simulation_day: int, removed_animal: li break removed_cow = self.cows.pop(remove_index) - self._record_sold_cow_stats(removed_cow, simulation_day) + removed_cow.sold_at_day = simulation_day + removed_cow.cull_reason = "culled for herd resize" animals_removed.append(removed_cow) return animals_removed @@ -1831,6 +1801,7 @@ def _update_sold_and_died_cow_statistics(self, sold_and_died_cows: list[Animal]) sum_cow_culling_age = self.herd_statistics.avg_cow_culling_age * self.herd_statistics.cow_herd_exit_num + sum( [cow.days_born for cow in sold_and_died_cows] ) + self.herd_statistics.cow_num -= len(sold_and_died_cows) self.herd_statistics.cow_herd_exit_num += len(sold_and_died_cows) self.herd_statistics.avg_cow_culling_age = ( (sum_cow_culling_age / self.herd_statistics.cow_herd_exit_num) @@ -1855,6 +1826,9 @@ def _update_sold_and_died_cow_statistics(self, sold_and_died_cows: list[Animal]) [cow for cow in sold_and_died_cows if cow.cull_reason == cull_reason] ) + oversupply_cows_num = sum(cow.cull_reason == animal_constants.OVERSUPPLY_CULL for cow in sold_and_died_cows) + self.herd_statistics.sold_cow_oversupply_num += oversupply_cows_num + sold_cows: list[Animal] = [cow for cow in sold_and_died_cows if cow.cull_reason != animal_constants.DEATH_CULL] self.herd_statistics.sold_cows_info += [ SoldAnimalTypedDict( 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 ee2f61efa7..aabd6b2bb1 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 @@ -572,14 +572,6 @@ def test_check_if_cows_need_to_be_sold_comprehensive(herd_manager: HerdManager, assert cow_protected_low_dim in herd_manager.cows assert cow_protected_high_preg in herd_manager.cows - assert herd_manager.herd_statistics.sold_cow_oversupply_num == 6 - assert herd_manager.herd_statistics.sold_cow_num == 6 - assert herd_manager.herd_statistics.cow_herd_exit_num == 16 - - assert len(herd_manager.herd_statistics.sold_cows_info) == 6 - assert herd_manager.herd_statistics.sold_cows_info[0]["id"] == cow_dnb_low_milk.id - assert herd_manager.herd_statistics.sold_cows_info[0]["sold_at_day"] == SIMULATION_DAY - herd_manager.herd_statistics.herd_num = 1 herd_manager.selling_threshold = 1.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 6bc3408498..f2d76e38a6 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 @@ -169,7 +169,7 @@ def test_calculate_cow_percentages(herd_manager: HerdManager, mock_herd: dict[st ( { animal_constants.DEATH_CULL: 0, - animal_constants.LOW_PROD_CULL: 0, + animal_constants.OVERSUPPLY_CULL: 0, animal_constants.LAMENESS_CULL: 0, animal_constants.INJURY_CULL: 0, animal_constants.MASTITIS_CULL: 0, @@ -180,7 +180,7 @@ def test_calculate_cow_percentages(herd_manager: HerdManager, mock_herd: dict[st 0, { animal_constants.DEATH_CULL: 0.0, - animal_constants.LOW_PROD_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, @@ -193,7 +193,7 @@ def test_calculate_cow_percentages(herd_manager: HerdManager, mock_herd: dict[st ( { animal_constants.DEATH_CULL: 5, - animal_constants.LOW_PROD_CULL: 0, + animal_constants.OVERSUPPLY_CULL: 0, animal_constants.LAMENESS_CULL: 0, animal_constants.INJURY_CULL: 0, animal_constants.MASTITIS_CULL: 0, @@ -204,7 +204,7 @@ def test_calculate_cow_percentages(herd_manager: HerdManager, mock_herd: dict[st 5, { animal_constants.DEATH_CULL: 100.0, - animal_constants.LOW_PROD_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, @@ -218,7 +218,7 @@ def test_calculate_cow_percentages(herd_manager: HerdManager, mock_herd: dict[st ( { animal_constants.DEATH_CULL: 5, - animal_constants.LOW_PROD_CULL: 5, + animal_constants.OVERSUPPLY_CULL: 5, animal_constants.LAMENESS_CULL: 0, animal_constants.INJURY_CULL: 0, animal_constants.MASTITIS_CULL: 0, @@ -229,7 +229,7 @@ def test_calculate_cow_percentages(herd_manager: HerdManager, mock_herd: dict[st 10, { animal_constants.DEATH_CULL: 50.0, - animal_constants.LOW_PROD_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, @@ -243,7 +243,7 @@ def test_calculate_cow_percentages(herd_manager: HerdManager, mock_herd: dict[st ( { animal_constants.DEATH_CULL: 3, - animal_constants.LOW_PROD_CULL: 2, + animal_constants.OVERSUPPLY_CULL: 2, animal_constants.LAMENESS_CULL: 0, animal_constants.INJURY_CULL: 0, animal_constants.MASTITIS_CULL: 0, @@ -254,7 +254,7 @@ def test_calculate_cow_percentages(herd_manager: HerdManager, mock_herd: dict[st 10, { animal_constants.DEATH_CULL: 30.0, - animal_constants.LOW_PROD_CULL: 20.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, @@ -269,7 +269,7 @@ def test_calculate_cow_percentages(herd_manager: HerdManager, mock_herd: dict[st ( { animal_constants.DEATH_CULL: 2, - animal_constants.LOW_PROD_CULL: 0, + animal_constants.OVERSUPPLY_CULL: 0, animal_constants.LAMENESS_CULL: 0, animal_constants.INJURY_CULL: 0, animal_constants.MASTITIS_CULL: 0, @@ -280,7 +280,7 @@ def test_calculate_cow_percentages(herd_manager: HerdManager, mock_herd: dict[st 10, { animal_constants.DEATH_CULL: 20.0, - animal_constants.LOW_PROD_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, @@ -533,7 +533,7 @@ def test_update_sold_and_died_cow_statistics( ) -> None: """Unit test for _update_sold_and_died_cow_statistics()""" cull_reasons = [ - animal_constants.LOW_PROD_CULL, + animal_constants.OVERSUPPLY_CULL, animal_constants.LAMENESS_CULL, animal_constants.INJURY_CULL, animal_constants.MASTITIS_CULL, @@ -604,7 +604,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.LOW_PROD_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), @@ -616,8 +616,8 @@ def test_update_sold_and_died_cow_statistics( expected_cull_reason_stats = { animal_constants.DEATH_CULL: current_cull_reason_stats[animal_constants.DEATH_CULL] + len([cow for cow in sold_and_died_cows if cow.cull_reason == animal_constants.DEATH_CULL]), - animal_constants.LOW_PROD_CULL: current_cull_reason_stats[animal_constants.LOW_PROD_CULL] - + len([cow for cow in sold_and_died_cows if cow.cull_reason == animal_constants.LOW_PROD_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] From 61a1b209dd69560584588addbbda18a6c5ce3c0e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Mar 2026 18:52:44 +0000 Subject: [PATCH 086/107] Apply Black Formatting --- RUFAS/biophysical/animal/herd_manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index c8d679fc52..1dbfd2dd0b 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -738,8 +738,7 @@ def _check_if_replacement_heifers_needed(self, time: RufasTime) -> list[Animal]: """ animals_added: list[Animal] = [] while ( - len(self.cows) + self.herd_statistics.bought_heifer_num < self.buying_threshold - and time.simulation_day > 1 + len(self.cows) + self.herd_statistics.bought_heifer_num < self.buying_threshold and time.simulation_day > 1 ): if len(self.replacement_market) == 0: break From 737122cd05215a7d24e81ac58aa2494ca47a0c1e Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Fri, 13 Mar 2026 23:06:21 +0900 Subject: [PATCH 087/107] Addressed final comments --- RUFAS/biophysical/animal/herd_manager.py | 8 +++++++- changelog.md | 2 +- .../test_herd_manager/test_herd_manager_daily_routines.py | 5 +---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 1dbfd2dd0b..8718681a11 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -707,7 +707,13 @@ def _check_if_cows_need_to_be_sold(self, simulation_day: int, removed_animal: li remove_index = self._get_cow_removal_index(removed_animal) if remove_index is None: - self.om.add_error("Unable to adjust herd size", "There are no cow that's qualified to be sold.", {}) + info_map = { + "class": self.__class__.__name__, + "function": self._check_if_cows_need_to_be_sold.__name__, + "simulation_day": simulation_day, + } + self.om.add_error("Unable to adjust herd size", "There are no cow that's qualified to be sold.", + info_map) break removed_cow = self.cows.pop(remove_index) diff --git a/changelog.md b/changelog.md index 3b79f3e479..181e385c3d 100644 --- a/changelog.md +++ b/changelog.md @@ -45,6 +45,7 @@ v1.0.0 - [2848](https://github.com/RuminantFarmSystems/MASM/pull/2848) - [minor change] [NoInputChange] [NoOutputChange] Justify `deepcopy()` usage. - [2843](https://github.com/RuminantFarmSystems/MASM/pull/2843) - [minor change] [NoInputChange] [NoOutputChange] Fix Simple `#noqa`s in codebase. - [2852](https://github.com/RuminantFarmSystems/MASM/pull/2852) - [minor change] [NoInputChange] [NoOutputChange] Fix AssertionError on `dev`. +- [2622](https://github.com/RuminantFarmSystems/MASM/pull/2622) - [minor change] [InputChange][OutputChange] Update culling logic to respond to number of available heifers. ### v1.0.0 @@ -288,7 +289,6 @@ v1.0.0 - [2618](https://github.com/RuminantFarmSystems/MASM/pull/2618) - [minor change] Removes outdated example regional feed inputs. - [2624](https://github.com/RuminantFarmSystems/MASM/pull/2624) - [minor change] [NoInputChange][NoOutputChange] Updates the PR template and changelog to reflect IO changes. - [1299](https://github.com/RuminantFarmSystems/MASM/pull/1299) - [minor change] [InputChange][OutputChange] Implements the Energy submodule of the EEE module with diesel consumption calculation. -- [2622](https://github.com/RuminantFarmSystems/MASM/pull/2622) - [minor change] [InputChange][OutputChange] Implement 305 days milk production logic and utilizing such merit to maintain herd size. - [2629](https://github.com/RuminantFarmSystems/MASM/pull/2629) - [minor change] [NoInputChange][NoOutputChange][Units][Reporting] Updates a couple units mappings that were incorrect/misspelled. - [2635](https://github.com/RuminantFarmSystems/MASM/pull/2635) - [minor change] [NoInputChange][NoOutputChange] Updates the ReadMe `Getting Started` section to be more straightforward about how to get RUFAS running. - [2634](https://github.com/RuminantFarmSystems/MASM/pull/2634) - [minor change] [NoInputChange][NoOutputChange][Animal] Reinstates functionality of report_daily_herd_total_ration method. 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 aabd6b2bb1..bf779b5391 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 @@ -582,10 +582,7 @@ def test_check_if_cows_need_to_be_sold_comprehensive(herd_manager: HerdManager, stuck_result = herd_manager._check_if_cows_need_to_be_sold(SIMULATION_DAY, []) assert len(stuck_result) == 0 - mock_om_add_error.assert_called_once_with( - "Unable to adjust herd size", "There are no cow that's qualified to be sold.", {} - ) - + mock_om_add_error.assert_called_once() def test_check_if_replacement_heifers_needed( mock_get_data_side_effect: list[Any], mocker: MockerFixture, mock_herd: dict[str, list[Animal]] From 1688dd2bc9868df77260b103236c0861a70398f7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 13 Mar 2026 14:08:06 +0000 Subject: [PATCH 088/107] Apply Black Formatting --- RUFAS/biophysical/animal/herd_manager.py | 5 +++-- .../test_herd_manager/test_herd_manager_daily_routines.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 8718681a11..fcb70f326d 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -712,8 +712,9 @@ def _check_if_cows_need_to_be_sold(self, simulation_day: int, removed_animal: li "function": self._check_if_cows_need_to_be_sold.__name__, "simulation_day": simulation_day, } - self.om.add_error("Unable to adjust herd size", "There are no cow that's qualified to be sold.", - info_map) + self.om.add_error( + "Unable to adjust herd size", "There are no cow that's qualified to be sold.", info_map + ) break removed_cow = self.cows.pop(remove_index) 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 bf779b5391..d1ed978627 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 @@ -584,6 +584,7 @@ def test_check_if_cows_need_to_be_sold_comprehensive(herd_manager: HerdManager, assert len(stuck_result) == 0 mock_om_add_error.assert_called_once() + def test_check_if_replacement_heifers_needed( mock_get_data_side_effect: list[Any], mocker: MockerFixture, mock_herd: dict[str, list[Animal]] ) -> None: From faac9b9a742e5db5949952727e63537c43d7f1b8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 23 Mar 2026 07:49:06 +0000 Subject: [PATCH 089/107] Apply Black Formatting From fac69590f20fef963cf6b2ca41d4b25b45d02ecc Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Mon, 23 Mar 2026 07:53:32 +0000 Subject: [PATCH 090/107] Update badges on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e5e7da272..71d943af31 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-1248%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1213%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems From 4cb5ef9b5a003f252422d2734121d832b26642d0 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Mon, 23 Mar 2026 17:25:50 +0900 Subject: [PATCH 091/107] Added warning for no production cows --- RUFAS/biophysical/animal/herd_manager.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index fcb70f326d..cc3da6cd42 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -626,6 +626,20 @@ def daily_routines( self.update_herd_statistics() + no_milk_cow_num = len([cow for cow in self.cows if cow.milk_production.daily_milk_produced < 0 and + cow.is_milking]) + + if no_milk_cow_num > 0: + self.om.add_warning( + f"Warning: Lactating cows with no production.", + f"There are {no_milk_cow_num} lactating cows with no milking production on simulation" + f" day {time.simulation_day}.", + info_map={ + "class": self.__class__.__name__, + "function": self.daily_routines.__name__, + "simulation_day": time.simulation_day, + }, + ) AnimalModuleReporter.report_enteric_methane_emission(enteric_methane_emission_by_pen) AnimalModuleReporter.report_daily_animal_population(self.herd_statistics, time.simulation_day) AnimalModuleReporter.report_herd_statistics_data(self.herd_statistics, time.simulation_day) From a37c302b544153065bb58cea11a7f49ac4ebf310 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 23 Mar 2026 08:27:50 +0000 Subject: [PATCH 092/107] Apply Black Formatting --- RUFAS/biophysical/animal/herd_manager.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index cc3da6cd42..c550235d94 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -626,8 +626,9 @@ def daily_routines( self.update_herd_statistics() - no_milk_cow_num = len([cow for cow in self.cows if cow.milk_production.daily_milk_produced < 0 and - cow.is_milking]) + no_milk_cow_num = len( + [cow for cow in self.cows if cow.milk_production.daily_milk_produced < 0 and cow.is_milking] + ) if no_milk_cow_num > 0: self.om.add_warning( @@ -637,7 +638,7 @@ def daily_routines( info_map={ "class": self.__class__.__name__, "function": self.daily_routines.__name__, - "simulation_day": time.simulation_day, + "simulation_day": time.simulation_day, }, ) AnimalModuleReporter.report_enteric_methane_emission(enteric_methane_emission_by_pen) From 1ac35116a0121d6d654c10859879cd2d80420f88 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Mon, 23 Mar 2026 08:31:54 +0000 Subject: [PATCH 093/107] Update badges on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 71d943af31..0d0afb872d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![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-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-1213%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) From 042e62a645737163eda0426cb6c4657b5a5322c4 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Mon, 23 Mar 2026 17:41:07 +0900 Subject: [PATCH 094/107] Fixed flake8 --- RUFAS/biophysical/animal/herd_manager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index cc3da6cd42..d00fb0594b 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -626,18 +626,18 @@ def daily_routines( self.update_herd_statistics() - no_milk_cow_num = len([cow for cow in self.cows if cow.milk_production.daily_milk_produced < 0 and - cow.is_milking]) + no_milk_cow_num = len([ + cow for cow in self.cows if cow.milk_production.daily_milk_produced < 0 and cow.is_milking]) if no_milk_cow_num > 0: self.om.add_warning( - f"Warning: Lactating cows with no production.", + "Warning: Lactating cows with no production.", f"There are {no_milk_cow_num} lactating cows with no milking production on simulation" f" day {time.simulation_day}.", info_map={ "class": self.__class__.__name__, "function": self.daily_routines.__name__, - "simulation_day": time.simulation_day, + "simulation_day": time.simulation_day, }, ) AnimalModuleReporter.report_enteric_methane_emission(enteric_methane_emission_by_pen) From 36f46c3dc187ff3488d0fb00f313d7edda7cfeaf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 23 Mar 2026 08:45:38 +0000 Subject: [PATCH 095/107] Apply Black Formatting --- RUFAS/biophysical/animal/herd_manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index d00fb0594b..c2fd4ebc5a 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -626,8 +626,9 @@ def daily_routines( self.update_herd_statistics() - no_milk_cow_num = len([ - cow for cow in self.cows if cow.milk_production.daily_milk_produced < 0 and cow.is_milking]) + no_milk_cow_num = len( + [cow for cow in self.cows if cow.milk_production.daily_milk_produced < 0 and cow.is_milking] + ) if no_milk_cow_num > 0: self.om.add_warning( From d4660bb465dcaf324612050fb42dd3952ebc05c1 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Mon, 23 Mar 2026 08:50:06 +0000 Subject: [PATCH 096/107] Update badges on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d0afb872d..71d943af31 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Flake8](https://img.shields.io/badge/Flake8-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![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-1213%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) From 0d65c2c8553829f848ee02c918389e4c28db8ca8 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Tue, 24 Mar 2026 11:34:17 +0900 Subject: [PATCH 097/107] Fixed a small bug --- RUFAS/biophysical/animal/herd_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index d00fb0594b..472668ae28 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -627,7 +627,7 @@ def daily_routines( self.update_herd_statistics() no_milk_cow_num = len([ - cow for cow in self.cows if cow.milk_production.daily_milk_produced < 0 and cow.is_milking]) + cow for cow in self.cows if cow.milk_production.daily_milk_produced == 0 and cow.is_milking]) if no_milk_cow_num > 0: self.om.add_warning( From 4850befbe18d1bfce1a4368bc8810202cb36eb0d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 24 Mar 2026 02:38:26 +0000 Subject: [PATCH 098/107] Apply Black Formatting --- RUFAS/biophysical/animal/herd_manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 472668ae28..640b9cb04e 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -626,8 +626,9 @@ def daily_routines( self.update_herd_statistics() - no_milk_cow_num = len([ - cow for cow in self.cows if cow.milk_production.daily_milk_produced == 0 and cow.is_milking]) + no_milk_cow_num = len( + [cow for cow in self.cows if cow.milk_production.daily_milk_produced == 0 and cow.is_milking] + ) if no_milk_cow_num > 0: self.om.add_warning( From e7436d08ef83d938c21befb6593b9c2bf5baab95 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Wed, 25 Mar 2026 13:02:40 +0900 Subject: [PATCH 099/107] Updated Julie's warning throwing criteria --- RUFAS/biophysical/animal/herd_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index 640b9cb04e..bc7f43b8a6 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -627,7 +627,8 @@ def daily_routines( self.update_herd_statistics() no_milk_cow_num = len( - [cow for cow in self.cows if cow.milk_production.daily_milk_produced == 0 and cow.is_milking] + [cow for cow in self.cows if cow.milk_production.daily_milk_produced == 0 and cow.is_milking and + cow.days_in_milk > 1] ) if no_milk_cow_num > 0: From 0b7f40b9d6560a678ef602bae15b7d9ec88599eb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 25 Mar 2026 04:05:53 +0000 Subject: [PATCH 100/107] Apply Black Formatting --- RUFAS/biophysical/animal/herd_manager.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/RUFAS/biophysical/animal/herd_manager.py b/RUFAS/biophysical/animal/herd_manager.py index bc7f43b8a6..8fa8eaefbd 100644 --- a/RUFAS/biophysical/animal/herd_manager.py +++ b/RUFAS/biophysical/animal/herd_manager.py @@ -627,8 +627,11 @@ def daily_routines( self.update_herd_statistics() no_milk_cow_num = len( - [cow for cow in self.cows if cow.milk_production.daily_milk_produced == 0 and cow.is_milking and - cow.days_in_milk > 1] + [ + cow + for cow in self.cows + if cow.milk_production.daily_milk_produced == 0 and cow.is_milking and cow.days_in_milk > 1 + ] ) if no_milk_cow_num > 0: From b52a078220c8076a8b60c97f63e664e46ed21808 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Wed, 25 Mar 2026 04:10:29 +0000 Subject: [PATCH 101/107] Update badges on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 71d943af31..a590494259 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-1213%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1214%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems From 3da40945b7b6457681b96514552d97890492432a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 26 Mar 2026 15:24:15 +0000 Subject: [PATCH 102/107] Apply Black Formatting From 1e1080b20f88eef31db102105eb0bd8b5b403159 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Thu, 26 Mar 2026 15:28:42 +0000 Subject: [PATCH 103/107] Update badges on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9cf019452d..86ac694827 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-1214%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1211%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems From bd18c4ff8a09a2547499fc8105a3a166023ddacb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 27 Mar 2026 14:26:00 +0000 Subject: [PATCH 104/107] Apply Black Formatting From 2cbcbb3c8e360679dec69dd65a1f41cc62f31371 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Fri, 27 Mar 2026 14:30:19 +0000 Subject: [PATCH 105/107] Update badges on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 86ac694827..bfec146ebb 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-1211%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1189%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems From b91280b3dff46e7b14fbc38d5982f4dc9595e69e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 31 Mar 2026 14:11:32 +0000 Subject: [PATCH 106/107] Apply Black Formatting From ea88a6f1f4b5e56d06ec3da00e5259c75d069052 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Tue, 31 Mar 2026 14:16:08 +0000 Subject: [PATCH 107/107] Update badges on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bfec146ebb..4ca366d044 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-1189%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1194%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems