diff --git a/RUFAS/biophysical/manure/storage/bedded_pack.py b/RUFAS/biophysical/manure/storage/bedded_pack.py index bad668b4bf..11893fe5c2 100644 --- a/RUFAS/biophysical/manure/storage/bedded_pack.py +++ b/RUFAS/biophysical/manure/storage/bedded_pack.py @@ -21,21 +21,25 @@ class Mixing(Enum): UNMIXED = False -BEDDED_PACK_MCF_TABLE: dict[Mixing, dict[tuple[float, float], float]] = { - Mixing.MIXED: { - (-math.inf, 4.6): 0.5, - (4.6, 5.8): 0.5, - (5.8, 13.9): 1.0, - (13.9, 25.1): 1.0, - (25.1, math.inf): 1.5, - }, - Mixing.UNMIXED: { - (-math.inf, 4.6): 21.0, - (4.6, 5.8): 26.0, - (5.8, 13.9): 37.0, - (13.9, 25.1): 41.0, - (25.1, math.inf): 74.0, - }, +BEDDED_PACK_MCF_MIXED: dict[tuple[float, float], float] = { + (-math.inf, 0): 0.5, + (0, 10): 0.5, + (10, 18): 1.0, + (18, math.inf): 1.5, +} + +BEDDED_PACK_MCF_UNMIXED_UNDER_30_DAYS: dict[tuple[float, float], float] = { + (-math.inf, 0): 2.75, + (0, 10): 2.75, + (10, 18): 6.5, + (18, math.inf): 18.0, +} + +BEDDED_PACK_MCF_UNMIXED_30_DAYS_OR_MORE: dict[tuple[float, float], float] = { + (-math.inf, 0): 14.0, + (0, 10): 21.0, + (10, 18): 37.0, + (18, math.inf): 73.0, } @@ -109,6 +113,7 @@ def process_manure(self, current_day_conditions: CurrentDayConditions, time: Ruf + self._manure_to_process.non_degradable_volatile_solids, self._determine_barn_temperature(manure_annual_temperature), self._manure_to_process.methane_production_potential, + self._storage_time_period, ) else: storage_methane = 0 @@ -376,7 +381,11 @@ def _calculate_bedded_pack_ammonia_emission(received_nitrogen: float, is_mixed: @staticmethod def calculate_bedded_pack_methane_emission( - is_mixed: bool, manure_volatile_solids: float, manure_temperature: float, methane_production_potential: float + is_mixed: bool, + manure_volatile_solids: float, + manure_temperature: float, + methane_production_potential: float, + storage_time_period: int | None, ) -> float: """ Calculates emission of methane on the current day based on methodology from IPCC 2019 @@ -392,6 +401,7 @@ def calculate_bedded_pack_methane_emission( The annual average temperature of the barn (Celsius). methane_production_potential : float Achievable emission of methane from dairy manure (m^3 methane / kg volatile solids). + storage_time_period : int | None Raises ------ @@ -416,7 +426,7 @@ def calculate_bedded_pack_methane_emission( raise ValueError(f"Manure volatile solids mass must be positive. Received {manure_volatile_solids}.") Bo = methane_production_potential methane_conversion_factor = BeddedPack.calculate_bedded_pack_methane_conversion_factor( - is_mixed, manure_temperature + is_mixed, manure_temperature, storage_time_period ) methane_emissions_in_kg = ( manure_volatile_solids * Bo * UserConstants.METHANE_FACTOR * methane_conversion_factor @@ -424,7 +434,9 @@ def calculate_bedded_pack_methane_emission( return methane_emissions_in_kg @staticmethod - def calculate_bedded_pack_methane_conversion_factor(is_mixed: bool, manure_temperature: float) -> float: + def calculate_bedded_pack_methane_conversion_factor( + is_mixed: bool, manure_temperature: float, storage_time_period: int | None + ) -> float: """ Calculates the Methane Conversion Factor (MCF) for the bedded pack based on annual temperature and whether or not the bedded pack is mixed. @@ -435,6 +447,8 @@ def calculate_bedded_pack_methane_conversion_factor(is_mixed: bool, manure_tempe Indicates whether this bedded pack is mixed or not. manure_temperature : float The annual average temperature of the barn (Celsius). + storage_time_period : int | None + How long manure is stored for before emptying the storage (days). None if the storage is never emptied. Raises ------ @@ -451,10 +465,17 @@ def calculate_bedded_pack_methane_conversion_factor(is_mixed: bool, manure_tempe 2024 USDA GHG inventory methods table 4-9. """ - mix = Mixing.MIXED if is_mixed else Mixing.UNMIXED - for (lower_bound, upper_bound), mcf in BEDDED_PACK_MCF_TABLE[mix].items(): + if is_mixed: + mcf_table = BEDDED_PACK_MCF_MIXED + elif storage_time_period is not None and storage_time_period < 30: + mcf_table = BEDDED_PACK_MCF_UNMIXED_UNDER_30_DAYS + else: + mcf_table = BEDDED_PACK_MCF_UNMIXED_30_DAYS_OR_MORE + + for (lower_bound, upper_bound), mcf in mcf_table.items(): if lower_bound < manure_temperature <= upper_bound: return mcf + OutputManager().add_error( "BeddedPack manure temp error", f"Temperature {manure_temperature}°C out of any defined bin", diff --git a/changelog_WIP.md b/changelog_WIP.md index 882ccd2dc7..467065fcbb 100644 --- a/changelog_WIP.md +++ b/changelog_WIP.md @@ -121,3 +121,4 @@ This **WIP Changelog** records development changes in progress and not yet inclu - [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. - [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`. +- [3285](https://github.com/RuminantFarmSystems/RuFaS/pull/3285) - [minor change] [Manure] [NoInputChange] [NoOutputChange] Adds MCF factor lookup table option for short-term storage duration `BeddedPack`. diff --git a/tests/test_biophysical/test_manure/test_storage/test_bedded_pack.py b/tests/test_biophysical/test_manure/test_storage/test_bedded_pack.py index 252991a4de..496e39f8cd 100644 --- a/tests/test_biophysical/test_manure/test_storage/test_bedded_pack.py +++ b/tests/test_biophysical/test_manure/test_storage/test_bedded_pack.py @@ -352,33 +352,42 @@ def test_calculate_bedded_pack_methane_emission(bedded_pack: BeddedPack, mocker: manure_volatile_solids = 1000.0 expected = (manure_volatile_solids * 0.24 * 0.67 * 1.0) / 100 - actual = bedded_pack.calculate_bedded_pack_methane_emission(True, manure_volatile_solids, 1.0, 0.24) + actual = bedded_pack.calculate_bedded_pack_methane_emission(True, manure_volatile_solids, 1.0, 0.24, 31) - mock_conversion_factor.assert_called_once_with(True, 1.0) + mock_conversion_factor.assert_called_once_with(True, 1.0, 31) assert actual == pytest.approx(expected) @pytest.mark.parametrize( - "is_mixed, manure_temperature, expected_mcf", + "is_mixed, manure_temperature, expected_mcf, storage_duration", [ # mixed - (True, -10.0, 0.5), # Falls in (-inf, 4.6] - (True, 0.0, 0.5), # “ - (True, 4.6, 0.5), # upper bound bin 1 - (True, 4.7, 0.5), # lower bound bin 2 - (True, 5.8, 0.5), # upper bound bin 2 (first match) - (True, 10.0, 1.0), # middle bin 3 - (True, 14.0, 1.0), # lower bound bin 4 - (True, 25.2, 1.5), # lower bound bin 5 - # unmixed - (False, -10.0, 21.0), - (False, 0.0, 21.0), - (False, 4.6, 21.0), - (False, 4.7, 26.0), - (False, 5.8, 26.0), - (False, 10.0, 37.0), - (False, 14.0, 41.0), - (False, 25.2, 74.0), + (True, -10.0, 0.5, 30), # Falls in (-inf, 4.6] + (True, 0.0, 0.5, 30), # “ + (True, 4.6, 0.5, 30), # upper bound bin 1 + (True, 4.7, 0.5, 30), # lower bound bin 2 + (True, 5.8, 0.5, 30), # upper bound bin 2 (first match) + (True, 10.0, 0.5, 30), # middle bin 3 + (True, 14.0, 1.0, 30), # lower bound bin 4 + (True, 25.2, 1.5, 30), # lower bound bin 5 + # unmixed, long storage duration + (False, -10.0, 14.0, 31), + (False, 0.0, 14.0, 31), + (False, 4.6, 21.0, 31), + (False, 4.7, 21.0, 31), + (False, 5.8, 21.0, None), + (False, 10.0, 21.0, None), + (False, 14.0, 37.0, None), + (False, 25.2, 73.0, None), + # unmixed, short storage duration + (False, -10.0, 2.75, 29), + (False, 0.0, 2.75, 29), + (False, 4.6, 2.75, 29), + (False, 4.7, 2.75, 29), + (False, 5.8, 2.75, 29), + (False, 10.0, 2.75, 29), + (False, 14.0, 6.5, 29), + (False, 25.2, 18.0, 29), ], ) def test_calculate_bedded_pack_mcf_returns_expected( @@ -386,9 +395,10 @@ def test_calculate_bedded_pack_mcf_returns_expected( is_mixed: bool, manure_temperature: float, expected_mcf: float, + storage_duration: int | None, ) -> None: """Tests calculate_bedded_pack_mcf_returns_expected().""" - result = bedded_pack.calculate_bedded_pack_methane_conversion_factor(is_mixed, manure_temperature) + result = bedded_pack.calculate_bedded_pack_methane_conversion_factor(is_mixed, manure_temperature, storage_duration) assert result == expected_mcf @@ -396,7 +406,7 @@ def test_calculate_bedded_pack_mcf_raises_for_temperature_gap(bedded_pack: Bedde """Tests calculate_bedded_pack_mcf_returns_expected() for fall back cases.""" mock_add_error = mocker.patch.object(bedded_pack._om, "add_error") with pytest.raises(ValueError) as excinfo: - bedded_pack.calculate_bedded_pack_methane_conversion_factor(True, math.nan) + bedded_pack.calculate_bedded_pack_methane_conversion_factor(True, math.nan, 30) assert "out of any defined bin" in str(excinfo.value) mock_add_error.assert_called_once()