Skip to content
61 changes: 41 additions & 20 deletions RUFAS/biophysical/manure/storage/bedded_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
------
Expand All @@ -416,15 +426,17 @@ 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
) / 100
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.
Expand All @@ -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
------
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions changelog_WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
54 changes: 32 additions & 22 deletions tests/test_biophysical/test_manure/test_storage/test_bedded_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,51 +352,61 @@ 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(
bedded_pack: BeddedPack,
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


def test_calculate_bedded_pack_mcf_raises_for_temperature_gap(bedded_pack: BeddedPack, mocker: MockerFixture) -> None:
"""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()