From 5f25095f9bb8246b1335d69239cf608a388703fa Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Tue, 8 Sep 2026 23:33:39 +0900 Subject: [PATCH 01/12] adds farm-specific genetic variance and correlation inputs to herd_information Exposes the CDCB national-average standard deviations and correlations of the TBV, permanent environmental, and temporary environmental distributions as optional animal inputs (herd_information) that default to the CDCB values when omitted. Genetics reads them from AnimalConfig instead of module constants. --- RUFAS/biophysical/animal/animal_config.py | 48 ++++++- .../animal/animal_genetics/animal_genetics.py | 45 ++++--- RUFAS/input/metadata/properties/default.json | 57 ++++++++ .../data/animal/example_freestall_animal.json | 11 +- .../data/animal/example_open_lot_animal.json | 11 +- .../animal_genetics/test_animal_genetics.py | 126 ++++++++++++++---- .../test_animal/test_animal_config.py | 74 +++++++++- 7 files changed, 315 insertions(+), 57 deletions(-) diff --git a/RUFAS/biophysical/animal/animal_config.py b/RUFAS/biophysical/animal/animal_config.py index a11a7cde68..55b9b57255 100644 --- a/RUFAS/biophysical/animal/animal_config.py +++ b/RUFAS/biophysical/animal/animal_config.py @@ -192,6 +192,33 @@ class AnimalConfig: Semen sire information used for genetic simulations and breeding selection. simulate_genetics : bool Whether genetic simulation functionality is enabled. + tbv_fat_std : float + Standard deviation of the true breeding value (TBV) for fat yield in the herd, (kg). Defaults to the + CDCB national average. + tbv_protein_std : float + Standard deviation of the true breeding value (TBV) for protein yield in the herd, (kg). Defaults to + the CDCB national average. + tbv_correlation : float + Correlation between the fat and protein true breeding values, (unitless). Defaults to the CDCB + national average. + permanent_environment_fat_std : float + Standard deviation of the permanent environmental effect on fat yield in the herd, (kg). Defaults to + the CDCB national average. + permanent_environment_protein_std : float + Standard deviation of the permanent environmental effect on protein yield in the herd, (kg). Defaults + to the CDCB national average. + permanent_environment_correlation : float + Correlation between the fat and protein permanent environmental effects, (unitless). Defaults to the + CDCB national average. + temporary_environment_fat_std : float + Standard deviation of the temporary environmental effect on fat yield in the herd, (kg). Defaults to + the CDCB national average. + temporary_environment_protein_std : float + Standard deviation of the temporary environmental effect on protein yield in the herd, (kg). Defaults + to the CDCB national average. + temporary_environment_correlation : float + Correlation between the fat and protein temporary environmental effects, (unitless). Defaults to the + CDCB national average. """ @@ -399,6 +426,15 @@ class AnimalConfig: average_phenotype: dict[str, dict[int, float]] = {} top_listing_semen: dict[str, dict[str, float]] = {} simulate_genetics: bool = False + tbv_fat_std: float = 25.8 + tbv_protein_std: float = 13.4 + tbv_correlation: float = 0.59 + permanent_environment_fat_std: float = 38.8 + permanent_environment_protein_std: float = 20.1 + permanent_environment_correlation: float = 0.95 + temporary_environment_fat_std: float = 64.5 + temporary_environment_protein_std: float = 33.4 + temporary_environment_correlation: float = 0.78 @classmethod def initialize_animal_config(cls) -> None: @@ -587,4 +623,14 @@ def initialize_animal_config(cls) -> None: for trait, values in top_listing_semen.items() if trait != "year_month" } - cls.simulate_genetics = animal_data["herd_information"]["simulate_genetics"] + herd_information = animal_data["herd_information"] + cls.simulate_genetics = herd_information["simulate_genetics"] + cls.tbv_fat_std = herd_information["tbv_fat_std"] + cls.tbv_protein_std = herd_information["tbv_protein_std"] + cls.tbv_correlation = herd_information["tbv_correlation"] + cls.permanent_environment_fat_std = herd_information["permanent_environment_fat_std"] + cls.permanent_environment_protein_std = herd_information["permanent_environment_protein_std"] + cls.permanent_environment_correlation = herd_information["permanent_environment_correlation"] + cls.temporary_environment_fat_std = herd_information["temporary_environment_fat_std"] + cls.temporary_environment_protein_std = herd_information["temporary_environment_protein_std"] + cls.temporary_environment_correlation = herd_information["temporary_environment_correlation"] diff --git a/RUFAS/biophysical/animal/animal_genetics/animal_genetics.py b/RUFAS/biophysical/animal/animal_genetics/animal_genetics.py index 9ab359b787..56ab45d2ff 100644 --- a/RUFAS/biophysical/animal/animal_genetics/animal_genetics.py +++ b/RUFAS/biophysical/animal/animal_genetics/animal_genetics.py @@ -7,15 +7,6 @@ from RUFAS.biophysical.animal.data_types.animal_types import AnimalType from RUFAS.util import Utility -TBV_FAT_STD = 25.8 -TBV_PROTEIN_STD = 13.4 -TBV_CORRELATION = 0.59 -E_PERMANENT_FAT_STD = 38.8 -E_PERMANENT_PROTEIN_STD = 20.1 -E_PERMANENT_CORRELATION = 0.95 -E_TEMPORARY_FAT_STD = 64.5 -E_TEMPORARY_PROTEIN_STD = 33.4 -E_TEMPORARY_CORRELATION = 0.78 FAT_ACCURACY_BY_PARITY = {0: 0.75, 1: 0.80, 2: 0.85, 3: 0.90} PROTEIN_ACCURACY_BY_PARITY = {0: 0.75, 1: 0.80, 2: 0.85, 3: 0.90} @@ -38,6 +29,10 @@ class Genetics: """ Genetic attributes of an animal. + The standard deviations and correlations of the true breeding value, permanent environmental effect, and + temporary environmental effect distributions are read from ``AnimalConfig``, where they are user inputs in + the ``herd_information`` section of the animal input that default to the CDCB national averages. + Attributes ---------- TBV_fat : float @@ -157,9 +152,9 @@ def recalculate_values_at_lactation_start( self.ranking_index = self._calculate_ranking_index() def _calculate_tbv_values(self) -> tuple[float, float]: - """Calculate TBV values for an animal entering the herd.""" + """Calculate TBV values for an animal entering the herd using the herd-level TBV distribution.""" tbv_fat, tbv_protein = Utility.generate_bivariate_random_numbers( - 0.0, 0.0, TBV_FAT_STD, TBV_PROTEIN_STD, TBV_CORRELATION + 0.0, 0.0, AnimalConfig.tbv_fat_std, AnimalConfig.tbv_protein_std, AnimalConfig.tbv_correlation ) return tbv_fat, tbv_protein @@ -205,31 +200,37 @@ def _calculate_newborn_calf_tbv_values( else: self.om.add_error("Newborn calf tbv calculation key error.", str(key_error), info_map) raise key_error - std_tbv_fat_national_average, std_tbv_protein_national_average = TBV_FAT_STD, TBV_PROTEIN_STD - mean_tbv_fat = (tbv_fat_top_semen + dam_tbv_fat) / 2 mean_tbv_protein = (tbv_protein_top_semen + dam_tbv_protein) / 2 - std_tbv_fat = np.sqrt(std_tbv_fat_national_average**2 / 2) - std_tbv_protein = np.sqrt(std_tbv_protein_national_average**2 / 2) + std_tbv_fat = np.sqrt(AnimalConfig.tbv_fat_std**2 / 2) + std_tbv_protein = np.sqrt(AnimalConfig.tbv_protein_std**2 / 2) tbv_fat, tbv_protein = Utility.generate_bivariate_random_numbers( - mean_tbv_fat, mean_tbv_protein, std_tbv_fat, std_tbv_protein, TBV_CORRELATION + mean_tbv_fat, mean_tbv_protein, std_tbv_fat, std_tbv_protein, AnimalConfig.tbv_correlation ) return tbv_fat, tbv_protein def _calculate_ep_values(self) -> tuple[float, float]: - """Calculate Permanent Environment Effect (E_permanent) values.""" + """Calculate Permanent Environment Effect (E_permanent) values using the herd-level distribution.""" ep_fat, ep_protein = Utility.generate_bivariate_random_numbers( - 0.0, 0.0, E_PERMANENT_FAT_STD, E_PERMANENT_PROTEIN_STD, E_PERMANENT_CORRELATION + 0.0, + 0.0, + AnimalConfig.permanent_environment_fat_std, + AnimalConfig.permanent_environment_protein_std, + AnimalConfig.permanent_environment_correlation, ) return ep_fat, ep_protein def _calculate_et_values(self) -> tuple[float, float]: - """Calculate Temporary Environment Effect (E_temporary) values.""" + """Calculate Temporary Environment Effect (E_temporary) values using the herd-level distribution.""" et_fat, et_protein = Utility.generate_bivariate_random_numbers( - 0.0, 0.0, E_TEMPORARY_FAT_STD, E_TEMPORARY_PROTEIN_STD, E_TEMPORARY_CORRELATION + 0.0, + 0.0, + AnimalConfig.temporary_environment_fat_std, + AnimalConfig.temporary_environment_protein_std, + AnimalConfig.temporary_environment_correlation, ) return et_fat, et_protein @@ -319,8 +320,8 @@ def _calculate_ebv_values( protein_accuracy**2 ) - std_ebv_fat = np.sqrt((1 - fat_accuracy**2) * (fat_accuracy**2) * TBV_FAT_STD) - std_ebv_protein = np.sqrt((1 - protein_accuracy**2) * (protein_accuracy**2) * TBV_PROTEIN_STD) + std_ebv_fat = np.sqrt((1 - fat_accuracy**2) * (fat_accuracy**2) * AnimalConfig.tbv_fat_std) + std_ebv_protein = np.sqrt((1 - protein_accuracy**2) * (protein_accuracy**2) * AnimalConfig.tbv_protein_std) noise_ebv_fat = np.random.normal(0.0, std_ebv_fat) noise_ebv_protein = np.random.normal(0.0, std_ebv_protein) diff --git a/RUFAS/input/metadata/properties/default.json b/RUFAS/input/metadata/properties/default.json index 3a4d2208b6..1dc9406f54 100644 --- a/RUFAS/input/metadata/properties/default.json +++ b/RUFAS/input/metadata/properties/default.json @@ -158,6 +158,63 @@ "type": "bool", "description": "Whether or not to simulate genetics for animals", "default": false + }, + "tbv_fat_std": { + "type": "number", + "description": "True Breeding Value Fat Standard Deviation (kg) -- The standard deviation of the true breeding value (TBV) for fat yield in the herd. Defaults to the CDCB national average", + "default": 25.8, + "minimum": 0 + }, + "tbv_protein_std": { + "type": "number", + "description": "True Breeding Value Protein Standard Deviation (kg) -- The standard deviation of the true breeding value (TBV) for protein yield in the herd. Defaults to the CDCB national average", + "default": 13.4, + "minimum": 0 + }, + "tbv_correlation": { + "type": "number", + "description": "True Breeding Value Correlation (unitless) -- The correlation between the fat and protein true breeding values (TBV). Defaults to the CDCB national average", + "default": 0.59, + "minimum": -1, + "maximum": 1 + }, + "permanent_environment_fat_std": { + "type": "number", + "description": "Permanent Environmental Effect Fat Standard Deviation (kg) -- The standard deviation of the permanent environmental effect on fat yield in the herd. Defaults to the CDCB national average", + "default": 38.8, + "minimum": 0 + }, + "permanent_environment_protein_std": { + "type": "number", + "description": "Permanent Environmental Effect Protein Standard Deviation (kg) -- The standard deviation of the permanent environmental effect on protein yield in the herd. Defaults to the CDCB national average", + "default": 20.1, + "minimum": 0 + }, + "permanent_environment_correlation": { + "type": "number", + "description": "Permanent Environmental Effect Correlation (unitless) -- The correlation between the fat and protein permanent environmental effects. Defaults to the CDCB national average", + "default": 0.95, + "minimum": -1, + "maximum": 1 + }, + "temporary_environment_fat_std": { + "type": "number", + "description": "Temporary Environmental Effect Fat Standard Deviation (kg) -- The standard deviation of the temporary environmental effect on fat yield in the herd. Defaults to the CDCB national average", + "default": 64.5, + "minimum": 0 + }, + "temporary_environment_protein_std": { + "type": "number", + "description": "Temporary Environmental Effect Protein Standard Deviation (kg) -- The standard deviation of the temporary environmental effect on protein yield in the herd. Defaults to the CDCB national average", + "default": 33.4, + "minimum": 0 + }, + "temporary_environment_correlation": { + "type": "number", + "description": "Temporary Environmental Effect Correlation (unitless) -- The correlation between the fat and protein temporary environmental effects. Defaults to the CDCB national average", + "default": 0.78, + "minimum": -1, + "maximum": 1 } }, "herd_initialization": { diff --git a/input/data/animal/example_freestall_animal.json b/input/data/animal/example_freestall_animal.json index b62c3d0914..4fef94511a 100644 --- a/input/data/animal/example_freestall_animal.json +++ b/input/data/animal/example_freestall_animal.json @@ -20,7 +20,16 @@ }, "milking_cow_fraction": 0.8356, "annual_milk_yield": null, - "simulate_genetics": false + "simulate_genetics": false, + "tbv_fat_std": 25.8, + "tbv_protein_std": 13.4, + "tbv_correlation": 0.59, + "permanent_environment_fat_std": 38.8, + "permanent_environment_protein_std": 20.1, + "permanent_environment_correlation": 0.95, + "temporary_environment_fat_std": 64.5, + "temporary_environment_protein_std": 33.4, + "temporary_environment_correlation": 0.78 }, "herd_initialization": { "initial_animal_num": 10000, diff --git a/input/data/animal/example_open_lot_animal.json b/input/data/animal/example_open_lot_animal.json index 66c0aaf167..2190ff9827 100644 --- a/input/data/animal/example_open_lot_animal.json +++ b/input/data/animal/example_open_lot_animal.json @@ -20,7 +20,16 @@ }, "milking_cow_fraction": 0.8356, "annual_milk_yield": 9928000, - "simulate_genetics": false + "simulate_genetics": false, + "tbv_fat_std": 25.8, + "tbv_protein_std": 13.4, + "tbv_correlation": 0.59, + "permanent_environment_fat_std": 38.8, + "permanent_environment_protein_std": 20.1, + "permanent_environment_correlation": 0.95, + "temporary_environment_fat_std": 64.5, + "temporary_environment_protein_std": 33.4, + "temporary_environment_correlation": 0.78 }, "herd_initialization": { "initial_animal_num": 10000, diff --git a/tests/test_biophysical/test_animal/animal_genetics/test_animal_genetics.py b/tests/test_biophysical/test_animal/animal_genetics/test_animal_genetics.py index beb26e9454..cb4d1897ba 100644 --- a/tests/test_biophysical/test_animal/animal_genetics/test_animal_genetics.py +++ b/tests/test_biophysical/test_animal/animal_genetics/test_animal_genetics.py @@ -6,18 +6,7 @@ from pytest_mock import MockerFixture from RUFAS.biophysical.animal.animal_config import AnimalConfig -from RUFAS.biophysical.animal.animal_genetics.animal_genetics import ( - Genetics, - TBV_CORRELATION, - TBV_FAT_STD, - TBV_PROTEIN_STD, - E_PERMANENT_FAT_STD, - E_PERMANENT_PROTEIN_STD, - E_PERMANENT_CORRELATION, - E_TEMPORARY_FAT_STD, - E_TEMPORARY_PROTEIN_STD, - E_TEMPORARY_CORRELATION, -) +from RUFAS.biophysical.animal.animal_genetics.animal_genetics import Genetics from RUFAS.biophysical.animal.data_types.animal_types import AnimalType @@ -151,14 +140,31 @@ def test_recalculate_values_at_lactation_start( mock_calculate_ranking_index.assert_called_once_with() -def test_calculate_tbv_values(genetics: Genetics, mocker: MockerFixture) -> None: - """Unit test for _calculate_tbv_values()""" +@pytest.mark.parametrize( + "tbv_fat_std, tbv_protein_std, tbv_correlation", + [ + (25.8, 13.4, 0.59), + (10.0, 5.0, 0.25), + ], + ids=["cdcb_national_average", "farm_specific"], +) +def test_calculate_tbv_values( + tbv_fat_std: float, + tbv_protein_std: float, + tbv_correlation: float, + genetics: Genetics, + mocker: MockerFixture, +) -> None: + """Unit test for _calculate_tbv_values(): the TBV distribution comes from the AnimalConfig inputs.""" + mocker.patch.object(AnimalConfig, "tbv_fat_std", tbv_fat_std) + mocker.patch.object(AnimalConfig, "tbv_protein_std", tbv_protein_std) + mocker.patch.object(AnimalConfig, "tbv_correlation", tbv_correlation) mock_generate_bivariate_random_numbers = mocker.patch.object( Utility, "generate_bivariate_random_numbers", return_value=(10.0, 20.0) ) genetics._calculate_tbv_values() mock_generate_bivariate_random_numbers.assert_called_once_with( - 0.0, 0.0, TBV_FAT_STD, TBV_PROTEIN_STD, TBV_CORRELATION + 0.0, 0.0, tbv_fat_std, tbv_protein_std, tbv_correlation ) @@ -198,7 +204,31 @@ def test_calculate_newborn_calf_tbv_values( expected_mean_tbv_protein, expected_std_tbv_fat, expected_std_tbv_protein, - TBV_CORRELATION, + AnimalConfig.tbv_correlation, + ) + + +def test_calculate_newborn_calf_tbv_values_uses_farm_specific_tbv_inputs( + genetics: Genetics, mocker: MockerFixture +) -> None: + """The newborn calf TBV spread is halved (in variance) from the user-input TBV standard deviations.""" + AnimalConfig.top_listing_semen["estimated_fat"] = {"2020-01": 13.0} + AnimalConfig.top_listing_semen["estimated_protein"] = {"2020-01": 18.0} + mocker.patch.object(AnimalConfig, "tbv_fat_std", 10.0) + mocker.patch.object(AnimalConfig, "tbv_protein_std", 5.0) + mocker.patch.object(AnimalConfig, "tbv_correlation", 0.25) + mock_generate_bivariate_random_numbers = mocker.patch.object( + Utility, "generate_bivariate_random_numbers", return_value=(10.0, 20.0) + ) + + genetics._calculate_newborn_calf_tbv_values(10.0, 20.0, "2020-01") + + mock_generate_bivariate_random_numbers.assert_called_once_with( + 11.5, + 19.0, + pytest.approx(7.0710678118654755), + pytest.approx(3.5355339059327378), + 0.25, ) @@ -230,26 +260,48 @@ def test_calculate_newborn_calf_tbv_values_key_error(genetics: Genetics, mocker: mock_generate.assert_not_called() -def test_calculate_ep_values(genetics: Genetics, mocker: MockerFixture) -> None: - """Unit test for _calculate_ep_values()""" +@pytest.mark.parametrize( + "fat_std, protein_std, correlation", + [ + (38.8, 20.1, 0.95), + (12.0, 6.0, 0.5), + ], + ids=["cdcb_national_average", "farm_specific"], +) +def test_calculate_ep_values( + fat_std: float, protein_std: float, correlation: float, genetics: Genetics, mocker: MockerFixture +) -> None: + """Unit test for _calculate_ep_values(): the E_permanent distribution comes from the AnimalConfig inputs.""" + mocker.patch.object(AnimalConfig, "permanent_environment_fat_std", fat_std) + mocker.patch.object(AnimalConfig, "permanent_environment_protein_std", protein_std) + mocker.patch.object(AnimalConfig, "permanent_environment_correlation", correlation) mock_generate_bivariate_random_numbers = mocker.patch.object( Utility, "generate_bivariate_random_numbers", return_value=(10.0, 20.0) ) genetics._calculate_ep_values() - mock_generate_bivariate_random_numbers.assert_called_once_with( - 0.0, 0.0, E_PERMANENT_FAT_STD, E_PERMANENT_PROTEIN_STD, E_PERMANENT_CORRELATION - ) + mock_generate_bivariate_random_numbers.assert_called_once_with(0.0, 0.0, fat_std, protein_std, correlation) -def test_calculate_et_values(genetics: Genetics, mocker: MockerFixture) -> None: - """Unit test for _calculate_et_values()""" +@pytest.mark.parametrize( + "fat_std, protein_std, correlation", + [ + (64.5, 33.4, 0.78), + (20.0, 10.0, 0.4), + ], + ids=["cdcb_national_average", "farm_specific"], +) +def test_calculate_et_values( + fat_std: float, protein_std: float, correlation: float, genetics: Genetics, mocker: MockerFixture +) -> None: + """Unit test for _calculate_et_values(): the E_temporary distribution comes from the AnimalConfig inputs.""" + mocker.patch.object(AnimalConfig, "temporary_environment_fat_std", fat_std) + mocker.patch.object(AnimalConfig, "temporary_environment_protein_std", protein_std) + mocker.patch.object(AnimalConfig, "temporary_environment_correlation", correlation) mock_generate_bivariate_random_numbers = mocker.patch.object( Utility, "generate_bivariate_random_numbers", return_value=(10.0, 20.0) ) genetics._calculate_et_values() - mock_generate_bivariate_random_numbers.assert_called_once_with( - 0.0, 0.0, E_TEMPORARY_FAT_STD, E_TEMPORARY_PROTEIN_STD, E_TEMPORARY_CORRELATION - ) + mock_generate_bivariate_random_numbers.assert_called_once_with(0.0, 0.0, fat_std, protein_std, correlation) @pytest.mark.parametrize( @@ -343,6 +395,24 @@ def test_calculate_ebv_values( assert ebv_protein == pytest.approx(expected_ebv_protein) +def test_calculate_ebv_values_uses_farm_specific_tbv_inputs(genetics: Genetics, mocker: MockerFixture) -> None: + """The EBV estimation noise is scaled by the user-input TBV standard deviations.""" + genetics.TBV_fat = 10.0 + genetics.TBV_protein = 20.0 + mocker.patch.object(AnimalConfig, "tbv_fat_std", 100.0) + mocker.patch.object(AnimalConfig, "tbv_protein_std", 50.0) + mock_np_random_normal = mocker.patch.object(numpy.random, "normal", side_effect=[0.0, 0.0]) + + genetics._calculate_ebv_values( + animal_type=AnimalType.CALF, parity=None, group_specific_TBV_fat_mean=1.1, group_specific_TBV_protein_mean=2.2 + ) + + assert mock_np_random_normal.call_args_list == [ + call(0.0, pytest.approx(4.960783708246107)), + call(0.0, pytest.approx(3.5078038001005702)), + ] + + @pytest.mark.parametrize( "ebv_fat, ebv_protein, expected_ranking_index", [ @@ -504,7 +574,7 @@ def test_calculate_newborn_calf_tbv_values_too_early(genetics: Genetics, mocker: expected_mean_protein, pytest.approx(18.243354954612926), pytest.approx(9.475230867899738), - TBV_CORRELATION, + AnimalConfig.tbv_correlation, ) mock_add_warning.assert_called_once() @@ -526,7 +596,7 @@ def test_calculate_newborn_calf_tbv_values_too_late(genetics: Genetics, mocker: expected_mean_protein, pytest.approx(18.243354954612926), pytest.approx(9.475230867899738), - TBV_CORRELATION, + AnimalConfig.tbv_correlation, ) mock_add_warning.assert_called_once() diff --git a/tests/test_biophysical/test_animal/test_animal/test_animal_config.py b/tests/test_biophysical/test_animal/test_animal/test_animal_config.py index 633ac25e0d..785aa4cbb9 100644 --- a/tests/test_biophysical/test_animal/test_animal/test_animal_config.py +++ b/tests/test_biophysical/test_animal/test_animal/test_animal_config.py @@ -44,6 +44,22 @@ def reset_animal_config_state() -> Generator[None, None, None]: setattr(AnimalConfig, name, value) +def _make_herd_information() -> dict[str, Any]: + """Builds the ``herd_information`` blob that ``AnimalConfig.initialize_animal_config()`` reads.""" + return { + "simulate_genetics": False, + "tbv_fat_std": 25.8, + "tbv_protein_std": 13.4, + "tbv_correlation": 0.59, + "permanent_environment_fat_std": 38.8, + "permanent_environment_protein_std": 20.1, + "permanent_environment_correlation": 0.95, + "temporary_environment_fat_std": 64.5, + "temporary_environment_protein_std": 33.4, + "temporary_environment_correlation": 0.78, + } + + def _make_base_animal_config(repro_sub_protocol: str, heifer_repro_method: str) -> dict[str, Any]: """Builds the ``animal_config`` blob that ``AnimalConfig.initialize_animal_config()`` reads.""" return { @@ -193,7 +209,7 @@ def test_initialize_animal_config_heifer_subprogram_and_core_fields( "methane_mitigation": { "methane_mitigation_method": "None", }, - "herd_information": {"simulate_genetics": False}, + "herd_information": _make_herd_information(), } def get_data_side_effect(key: str) -> Any: @@ -262,7 +278,7 @@ def test_initialize_animal_config_selects_dose_of_chosen_mitigation_method( "essential_oils_additive_amount": 50, "seaweed_additive_amount": 55, }, - "herd_information": {"simulate_genetics": False}, + "herd_information": _make_herd_information(), } def get_data_side_effect(key: str) -> Any: @@ -297,7 +313,7 @@ def test_initialize_animal_config_warns_when_selected_mitigation_dose_field_is_m "methane_model": {"dummy": "model"}, # "3-NOP" is selected, but "3-NOP_additive_amount" is absent from the blob. "methane_mitigation": {"methane_mitigation_method": "3-NOP"}, - "herd_information": {"simulate_genetics": False}, + "herd_information": _make_herd_information(), } def get_data_side_effect(key: str) -> Any: @@ -450,7 +466,7 @@ def test_initialize_animal_config_adds_warning_when_third_check_after_or_on_dryo "methane_mitigation": { "methane_mitigation_method": "None", }, - "herd_information": {"simulate_genetics": False}, + "herd_information": _make_herd_information(), } def get_data_side_effect(key: str) -> Any: @@ -474,3 +490,53 @@ def get_data_side_effect(key: str) -> Any: warning_args, warning_kwargs = mock_om.add_warning.call_args assert "3rd pregnancy check day >=" in warning_args[0] + + +def test_initialize_animal_config_reads_genetics_inputs(mocker: pytest_mock.MockerFixture) -> None: + """The farm-specific genetic distribution inputs in ``herd_information`` are read into ``AnimalConfig``.""" + mock_im_cls = mocker.patch("RUFAS.biophysical.animal.animal_config.InputManager") + mocker.patch("RUFAS.biophysical.animal.animal_config.OutputManager") + + mock_im = mock_im_cls.return_value + + animal_data = { + "animal_config": _make_base_animal_config("5dCG2P", "TAI"), + "methane_model": {"dummy": "model"}, + "methane_mitigation": {"methane_mitigation_method": "None"}, + "herd_information": { + "simulate_genetics": True, + "tbv_fat_std": 1.1, + "tbv_protein_std": 2.2, + "tbv_correlation": 0.3, + "permanent_environment_fat_std": 4.4, + "permanent_environment_protein_std": 5.5, + "permanent_environment_correlation": 0.6, + "temporary_environment_fat_std": 7.7, + "temporary_environment_protein_std": 8.8, + "temporary_environment_correlation": 0.9, + }, + } + + def get_data_side_effect(key: str) -> Any: + if key == "animal": + return animal_data + if key == "feed.ration_formulation_parameters.milk_reduction_maximum": + return 1.23 + if key in ("animal_mean_phenotype", "animal_top_listing_semen"): + return {} + raise KeyError(key) + + mock_im.get_data.side_effect = get_data_side_effect + + AnimalConfig.initialize_animal_config() + + assert AnimalConfig.simulate_genetics is True + assert AnimalConfig.tbv_fat_std == 1.1 + assert AnimalConfig.tbv_protein_std == 2.2 + assert AnimalConfig.tbv_correlation == 0.3 + assert AnimalConfig.permanent_environment_fat_std == 4.4 + assert AnimalConfig.permanent_environment_protein_std == 5.5 + assert AnimalConfig.permanent_environment_correlation == 0.6 + assert AnimalConfig.temporary_environment_fat_std == 7.7 + assert AnimalConfig.temporary_environment_protein_std == 8.8 + assert AnimalConfig.temporary_environment_correlation == 0.9 From 2eca8c05d6ca80def5972ac5a683a05536ec09e0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 8 Sep 2026 14:37:30 +0000 Subject: [PATCH 02/12] Apply Black Formatting From cf8a1dc9aaa3cdaacff1cff6a14172f9d58bba82 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Tue, 8 Sep 2026 23:36:09 +0900 Subject: [PATCH 03/12] adds changelog entry for PR 3257 --- changelog_WIP.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog_WIP.md b/changelog_WIP.md index dcb8212dfb..7326008a3a 100644 --- a/changelog_WIP.md +++ b/changelog_WIP.md @@ -119,3 +119,4 @@ This **WIP Changelog** records development changes in progress and not yet inclu - [3161](https://github.com/RuminantFarmSystems/RuFaS/pull/3161) - [minor change] [Docs] [Governance] [NoInputChange] [NoOutputChange] Adds Strategy.md defining the RuFaS vision, mission, strategic pillars, core values, and operating principles, and aligns README.md, CONTRIBUTING.md, and FORKING.md with the approved strategic direction. - [3223](https://github.com/RuminantFarmSystems/RuFaS/pull/3223) - [minor change] [PostProcessing] [OutputManager] [NoInputChange] [NoOutputChange] Establishes new overhauled version of OutputManager and the subclasses it oversees. - [3235](https://github.com/RuminantFarmSystems/RuFaS/pull/3235) - [minor change] [Dependabot] [NoInputChange] [NoOutputChange] Updates file-target of dependabot-change PRs for tagging dev-team members for review. +- [3257](https://github.com/RuminantFarmSystems/RuFaS/pull/3257) - [minor change] [Animal] [InputChange] [NoOutputChange] Adds optional farm-specific standard deviation and correlation inputs for the TBV, permanent, and temporary environmental effect genetic distributions to `herd_information`, defaulting to the CDCB national averages. From 879b7d05845b842d4a7f520a21ff44c1ed0fe98e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 8 Sep 2026 14:45:09 +0000 Subject: [PATCH 04/12] Apply Black Formatting From ad18257b4946f1544902d9894461a47a014d560e Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Fri, 11 Sep 2026 15:37:45 +0900 Subject: [PATCH 05/12] Addressed Allister's suggestion --- RUFAS/biophysical/animal/animal_config.py | 23 ++++++++++--------- .../animal/animal_genetics/animal_genetics.py | 2 +- RUFAS/input/metadata/properties/default.json | 6 ++++- .../data/animal/example_freestall_animal.json | 4 +++- .../data/animal/example_open_lot_animal.json | 4 +++- .../test_animal/test_animal_config.py | 23 +++++++++++-------- 6 files changed, 37 insertions(+), 25 deletions(-) diff --git a/RUFAS/biophysical/animal/animal_config.py b/RUFAS/biophysical/animal/animal_config.py index 55b9b57255..e6bd432ee8 100644 --- a/RUFAS/biophysical/animal/animal_config.py +++ b/RUFAS/biophysical/animal/animal_config.py @@ -623,14 +623,15 @@ def initialize_animal_config(cls) -> None: for trait, values in top_listing_semen.items() if trait != "year_month" } - herd_information = animal_data["herd_information"] - cls.simulate_genetics = herd_information["simulate_genetics"] - cls.tbv_fat_std = herd_information["tbv_fat_std"] - cls.tbv_protein_std = herd_information["tbv_protein_std"] - cls.tbv_correlation = herd_information["tbv_correlation"] - cls.permanent_environment_fat_std = herd_information["permanent_environment_fat_std"] - cls.permanent_environment_protein_std = herd_information["permanent_environment_protein_std"] - cls.permanent_environment_correlation = herd_information["permanent_environment_correlation"] - cls.temporary_environment_fat_std = herd_information["temporary_environment_fat_std"] - cls.temporary_environment_protein_std = herd_information["temporary_environment_protein_std"] - cls.temporary_environment_correlation = herd_information["temporary_environment_correlation"] + cls.simulate_genetics = animal_data["herd_information"]["simulate_genetics"] + + genetics_data = animal_data["genetics"] + cls.tbv_fat_std = genetics_data["tbv_fat_std"] + cls.tbv_protein_std = genetics_data["tbv_protein_std"] + cls.tbv_correlation = genetics_data["tbv_correlation"] + cls.permanent_environment_fat_std = genetics_data["permanent_environment_fat_std"] + cls.permanent_environment_protein_std = genetics_data["permanent_environment_protein_std"] + cls.permanent_environment_correlation = genetics_data["permanent_environment_correlation"] + cls.temporary_environment_fat_std = genetics_data["temporary_environment_fat_std"] + cls.temporary_environment_protein_std = genetics_data["temporary_environment_protein_std"] + cls.temporary_environment_correlation = genetics_data["temporary_environment_correlation"] diff --git a/RUFAS/biophysical/animal/animal_genetics/animal_genetics.py b/RUFAS/biophysical/animal/animal_genetics/animal_genetics.py index 56ab45d2ff..02c79c2116 100644 --- a/RUFAS/biophysical/animal/animal_genetics/animal_genetics.py +++ b/RUFAS/biophysical/animal/animal_genetics/animal_genetics.py @@ -31,7 +31,7 @@ class Genetics: The standard deviations and correlations of the true breeding value, permanent environmental effect, and temporary environmental effect distributions are read from ``AnimalConfig``, where they are user inputs in - the ``herd_information`` section of the animal input that default to the CDCB national averages. + the ``genetics`` section of the animal input that default to the CDCB national averages. Attributes ---------- diff --git a/RUFAS/input/metadata/properties/default.json b/RUFAS/input/metadata/properties/default.json index 1dc9406f54..00a51547be 100644 --- a/RUFAS/input/metadata/properties/default.json +++ b/RUFAS/input/metadata/properties/default.json @@ -158,7 +158,11 @@ "type": "bool", "description": "Whether or not to simulate genetics for animals", "default": false - }, + } + }, + "genetics": { + "type": "object", + "description": "Genetics -- Farm-specific standard deviations and correlations of the genetic distributions used when simulate_genetics is true. Omitted values default to the CDCB national averages", "tbv_fat_std": { "type": "number", "description": "True Breeding Value Fat Standard Deviation (kg) -- The standard deviation of the true breeding value (TBV) for fat yield in the herd. Defaults to the CDCB national average", diff --git a/input/data/animal/example_freestall_animal.json b/input/data/animal/example_freestall_animal.json index 4fef94511a..62c87f3e49 100644 --- a/input/data/animal/example_freestall_animal.json +++ b/input/data/animal/example_freestall_animal.json @@ -20,7 +20,9 @@ }, "milking_cow_fraction": 0.8356, "annual_milk_yield": null, - "simulate_genetics": false, + "simulate_genetics": false + }, + "genetics": { "tbv_fat_std": 25.8, "tbv_protein_std": 13.4, "tbv_correlation": 0.59, diff --git a/input/data/animal/example_open_lot_animal.json b/input/data/animal/example_open_lot_animal.json index 2190ff9827..11e1353b09 100644 --- a/input/data/animal/example_open_lot_animal.json +++ b/input/data/animal/example_open_lot_animal.json @@ -20,7 +20,9 @@ }, "milking_cow_fraction": 0.8356, "annual_milk_yield": 9928000, - "simulate_genetics": false, + "simulate_genetics": false + }, + "genetics": { "tbv_fat_std": 25.8, "tbv_protein_std": 13.4, "tbv_correlation": 0.59, diff --git a/tests/test_biophysical/test_animal/test_animal/test_animal_config.py b/tests/test_biophysical/test_animal/test_animal/test_animal_config.py index 785aa4cbb9..e4b9ea66d3 100644 --- a/tests/test_biophysical/test_animal/test_animal/test_animal_config.py +++ b/tests/test_biophysical/test_animal/test_animal/test_animal_config.py @@ -44,10 +44,9 @@ def reset_animal_config_state() -> Generator[None, None, None]: setattr(AnimalConfig, name, value) -def _make_herd_information() -> dict[str, Any]: - """Builds the ``herd_information`` blob that ``AnimalConfig.initialize_animal_config()`` reads.""" +def _make_genetics() -> dict[str, Any]: + """Builds the ``genetics`` blob that ``AnimalConfig.initialize_animal_config()`` reads.""" return { - "simulate_genetics": False, "tbv_fat_std": 25.8, "tbv_protein_std": 13.4, "tbv_correlation": 0.59, @@ -209,7 +208,8 @@ def test_initialize_animal_config_heifer_subprogram_and_core_fields( "methane_mitigation": { "methane_mitigation_method": "None", }, - "herd_information": _make_herd_information(), + "herd_information": {"simulate_genetics": False}, + "genetics": _make_genetics(), } def get_data_side_effect(key: str) -> Any: @@ -278,7 +278,8 @@ def test_initialize_animal_config_selects_dose_of_chosen_mitigation_method( "essential_oils_additive_amount": 50, "seaweed_additive_amount": 55, }, - "herd_information": _make_herd_information(), + "herd_information": {"simulate_genetics": False}, + "genetics": _make_genetics(), } def get_data_side_effect(key: str) -> Any: @@ -313,7 +314,8 @@ def test_initialize_animal_config_warns_when_selected_mitigation_dose_field_is_m "methane_model": {"dummy": "model"}, # "3-NOP" is selected, but "3-NOP_additive_amount" is absent from the blob. "methane_mitigation": {"methane_mitigation_method": "3-NOP"}, - "herd_information": _make_herd_information(), + "herd_information": {"simulate_genetics": False}, + "genetics": _make_genetics(), } def get_data_side_effect(key: str) -> Any: @@ -466,7 +468,8 @@ def test_initialize_animal_config_adds_warning_when_third_check_after_or_on_dryo "methane_mitigation": { "methane_mitigation_method": "None", }, - "herd_information": _make_herd_information(), + "herd_information": {"simulate_genetics": False}, + "genetics": _make_genetics(), } def get_data_side_effect(key: str) -> Any: @@ -493,7 +496,7 @@ def get_data_side_effect(key: str) -> Any: def test_initialize_animal_config_reads_genetics_inputs(mocker: pytest_mock.MockerFixture) -> None: - """The farm-specific genetic distribution inputs in ``herd_information`` are read into ``AnimalConfig``.""" + """The farm-specific genetic distribution inputs in the ``genetics`` section are read into ``AnimalConfig``.""" mock_im_cls = mocker.patch("RUFAS.biophysical.animal.animal_config.InputManager") mocker.patch("RUFAS.biophysical.animal.animal_config.OutputManager") @@ -503,8 +506,8 @@ def test_initialize_animal_config_reads_genetics_inputs(mocker: pytest_mock.Mock "animal_config": _make_base_animal_config("5dCG2P", "TAI"), "methane_model": {"dummy": "model"}, "methane_mitigation": {"methane_mitigation_method": "None"}, - "herd_information": { - "simulate_genetics": True, + "herd_information": {"simulate_genetics": True}, + "genetics": { "tbv_fat_std": 1.1, "tbv_protein_std": 2.2, "tbv_correlation": 0.3, From 4981d5449d67fefa6d34aed04e9a7495196bbaae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 14 Sep 2026 06:43:24 +0000 Subject: [PATCH 06/12] Apply Black Formatting From 1876e8195a99e33bbdde14342e81bed41ad169f2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 15 Sep 2026 08:28:17 +0000 Subject: [PATCH 07/12] Apply Black Formatting From a9bd5dfc616da15433deaf3bbf99a130e9bf0975 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 16 Sep 2026 04:56:50 +0000 Subject: [PATCH 08/12] Apply Black Formatting From 62d4bd341daa61b40ee9f67ec80c634e340977a0 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Wed, 16 Sep 2026 05:01:39 +0000 Subject: [PATCH 09/12] Update badges on README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c58c0facf1..ed66b0c6ba 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![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-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) +[![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-1164%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) From 6772f7744ed0e9846076a7425ca3d5dca111463e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 17 Sep 2026 09:04:35 +0000 Subject: [PATCH 10/12] Apply Black Formatting From 38916ca5cf4e42de75e3f0d6b99c4f4e1ee824b6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 22 Sep 2026 07:36:34 +0000 Subject: [PATCH 11/12] Apply Black Formatting From 209cfd97e688c680c19a901359b8e99cacf2dbb1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 24 Sep 2026 07:54:18 +0000 Subject: [PATCH 12/12] Apply Black Formatting