diff --git a/RUFAS/biophysical/animal/animal_config.py b/RUFAS/biophysical/animal/animal_config.py index a11a7cde68..e6bd432ee8 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: @@ -588,3 +624,14 @@ def initialize_animal_config(cls) -> None: if trait != "year_month" } 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 9ab359b787..02c79c2116 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 ``genetics`` 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 82a77509a4..22ce7f470f 100644 --- a/RUFAS/input/metadata/properties/default.json +++ b/RUFAS/input/metadata/properties/default.json @@ -160,6 +160,67 @@ "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", + "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": { "type": "object", "description": "Animal generation related inputs", diff --git a/changelog_WIP.md b/changelog_WIP.md index 0a3f98f343..07ca95e964 100644 --- a/changelog_WIP.md +++ b/changelog_WIP.md @@ -121,6 +121,7 @@ This **WIP Changelog** records development changes in progress and not yet inclu - [3237](https://github.com/RuminantFarmSystems/RuFaS/pull/3237) - [minor change] [EEE] [NoInputChange] [NoOutputChange] Extracts repetitive logic from the farmgrown feed emissions calculation and reporting functions. - [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. - [3256](https://github.com/RuminantFarmSystems/RuFaS/pull/3256) - [minor change] [Branch Alignment] [NoInputChange] [NoOutputChange] Aligning `dev` branch with bug-fixing code from PR 3214 that was merged into `test`. - [3260](https://github.com/RuminantFarmSystems/RuFaS/pull/3260) - [minor change] [OutputManager] [NoInputChange] [NoOutputChange] Removes duplicative `report` naming mechanism in `OutputManager`. - [3275](https://github.com/RuminantFarmSystems/RuFaS/pull/3260) - [minor change] [E2E Testing] [NoInputChange] [NoOutputChange] Removes `deepdiff` check from the process to update e2e expected results. diff --git a/input/data/animal/example_freestall_animal.json b/input/data/animal/example_freestall_animal.json index b62c3d0914..62c87f3e49 100644 --- a/input/data/animal/example_freestall_animal.json +++ b/input/data/animal/example_freestall_animal.json @@ -22,6 +22,17 @@ "annual_milk_yield": null, "simulate_genetics": false }, + "genetics": { + "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, "simulation_days": 5000 diff --git a/input/data/animal/example_open_lot_animal.json b/input/data/animal/example_open_lot_animal.json index 66c0aaf167..11e1353b09 100644 --- a/input/data/animal/example_open_lot_animal.json +++ b/input/data/animal/example_open_lot_animal.json @@ -22,6 +22,17 @@ "annual_milk_yield": 9928000, "simulate_genetics": false }, + "genetics": { + "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, "simulation_days": 5000 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..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,6 +44,21 @@ def reset_animal_config_state() -> Generator[None, None, None]: setattr(AnimalConfig, name, value) +def _make_genetics() -> dict[str, Any]: + """Builds the ``genetics`` blob that ``AnimalConfig.initialize_animal_config()`` reads.""" + return { + "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 { @@ -194,6 +209,7 @@ def test_initialize_animal_config_heifer_subprogram_and_core_fields( "methane_mitigation_method": "None", }, "herd_information": {"simulate_genetics": False}, + "genetics": _make_genetics(), } def get_data_side_effect(key: str) -> Any: @@ -263,6 +279,7 @@ def test_initialize_animal_config_selects_dose_of_chosen_mitigation_method( "seaweed_additive_amount": 55, }, "herd_information": {"simulate_genetics": False}, + "genetics": _make_genetics(), } def get_data_side_effect(key: str) -> Any: @@ -298,6 +315,7 @@ def test_initialize_animal_config_warns_when_selected_mitigation_dose_field_is_m # "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}, + "genetics": _make_genetics(), } def get_data_side_effect(key: str) -> Any: @@ -451,6 +469,7 @@ def test_initialize_animal_config_adds_warning_when_third_check_after_or_on_dryo "methane_mitigation_method": "None", }, "herd_information": {"simulate_genetics": False}, + "genetics": _make_genetics(), } def get_data_side_effect(key: str) -> Any: @@ -474,3 +493,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 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") + + 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}, + "genetics": { + "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