Skip to content

Commit c2aa4b5

Browse files
Merge pull request #894 from ImperialCollegeLondon/887-animal-model---handling-starvation-during-ontogeny
887 animal model handling starvation during ontogeny
2 parents 5a41889 + 46ea0a8 commit c2aa4b5

File tree

4 files changed

+163
-10
lines changed

4 files changed

+163
-10
lines changed

tests/models/animals/test_animal_cohorts.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3097,3 +3097,45 @@ def test_get_litter_pools(
30973097
result = cohort.get_litter_pools(test_litter_pools)
30983098

30993099
assert len(result) == expected
3100+
3101+
@pytest.mark.parametrize(
3102+
"carbon, nitrogen, phosphorus, initial_largest_mass, expected_largest_mass",
3103+
[
3104+
# Grows, still under adult mass
3105+
(6.0, 1.0, 0.5, 5.0, 7.5),
3106+
# Grows past adult mass, should cap
3107+
(50.0, 10.0, 5.0, 20.0, "cap_to_adult"),
3108+
# No growth, mass lower than previous largest
3109+
(4.0, 0.5, 0.2, 10.0, 10.0),
3110+
],
3111+
)
3112+
def test_update_largest_mass(
3113+
self,
3114+
herbivore_cohort_instance,
3115+
carbon,
3116+
nitrogen,
3117+
phosphorus,
3118+
initial_largest_mass,
3119+
expected_largest_mass,
3120+
):
3121+
"""Test update_largest_mass."""
3122+
3123+
# Set up current mass via mass_cnp
3124+
herbivore_cohort_instance.mass_cnp.carbon = carbon
3125+
herbivore_cohort_instance.mass_cnp.nitrogen = nitrogen
3126+
herbivore_cohort_instance.mass_cnp.phosphorus = phosphorus
3127+
3128+
# Set initial largest_mass_achieved
3129+
herbivore_cohort_instance.largest_mass_achieved = initial_largest_mass
3130+
3131+
# Call update
3132+
herbivore_cohort_instance.update_largest_mass()
3133+
3134+
# Determine expected value
3135+
if expected_largest_mass == "cap_to_adult":
3136+
expected = herbivore_cohort_instance.functional_group.adult_mass
3137+
else:
3138+
expected = expected_largest_mass
3139+
3140+
# Assertion
3141+
assert herbivore_cohort_instance.largest_mass_achieved == expected

tests/models/animals/test_animal_model.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,8 @@ def test_update_method_sequence(self, mocker, prepared_animal_model_instance):
165165
"metamorphose_community",
166166
"metabolize_community",
167167
"inflict_non_predation_mortality_community",
168-
"remove_dead_cohort_community",
169-
"increase_age_community",
168+
"update_community_bookkeeping",
169+
"update_cohort_bookkeeping",
170170
]
171171

172172
# Setup mock methods using spy on the prepared_animal_model_instance itself
@@ -2164,3 +2164,65 @@ def test_create_new_cohort_routing(
21642164
else:
21652165
assert cohort.id in animal_model_instance.active_cohorts
21662166
spy_occ.assert_called_once_with(cohort, 0)
2167+
2168+
def test_update_community_bookkeeping(self, mocker, prepared_animal_model_instance):
2169+
"""Test update_community_bookkeeping."""
2170+
2171+
# Spy on the three submethods
2172+
mocker.spy(prepared_animal_model_instance, "update_migrated_and_aquatic")
2173+
mocker.spy(prepared_animal_model_instance, "reintegrate_community")
2174+
mocker.spy(prepared_animal_model_instance, "remove_dead_cohort_community")
2175+
2176+
# Call the bookkeeping method
2177+
prepared_animal_model_instance.update_community_bookkeeping(
2178+
dt=np.timedelta64(1, "D")
2179+
)
2180+
2181+
# Assert each method was called exactly once
2182+
assert (
2183+
prepared_animal_model_instance.update_migrated_and_aquatic.call_count == 1
2184+
)
2185+
assert prepared_animal_model_instance.reintegrate_community.call_count == 1
2186+
assert (
2187+
prepared_animal_model_instance.remove_dead_cohort_community.call_count == 1
2188+
)
2189+
2190+
def test_update_cohort_bookkeeping(self, mocker, prepared_animal_model_instance):
2191+
"""Test that update_cohort_bookkeeping calls both age and ontogeny methods."""
2192+
2193+
# Spy on the model-level methods
2194+
mocker.spy(prepared_animal_model_instance, "increase_age_community")
2195+
mocker.spy(prepared_animal_model_instance, "handle_ontogeny")
2196+
2197+
# Call the method
2198+
prepared_animal_model_instance.update_cohort_bookkeeping(
2199+
dt=np.timedelta64(1, "M")
2200+
)
2201+
2202+
# Assert both were called once
2203+
assert prepared_animal_model_instance.increase_age_community.call_count == 1
2204+
assert prepared_animal_model_instance.handle_ontogeny.call_count == 1
2205+
2206+
def test_handle_ontogeny_calls_update_on_immature_cohorts(
2207+
self, mocker, prepared_animal_model_instance
2208+
):
2209+
"""Test handle_ontogeny."""
2210+
2211+
# Create two mock cohorts
2212+
mock_mature = mocker.Mock()
2213+
mock_mature.is_mature = True
2214+
mock_immature = mocker.Mock()
2215+
mock_immature.is_mature = False
2216+
2217+
# Add them to active_cohorts
2218+
prepared_animal_model_instance.active_cohorts = {
2219+
"mature": mock_mature,
2220+
"immature": mock_immature,
2221+
}
2222+
2223+
# Call the method
2224+
prepared_animal_model_instance.handle_ontogeny()
2225+
2226+
# Assert that only the immature cohort's update_largest_mass was called
2227+
mock_immature.update_largest_mass.assert_called_once()
2228+
mock_mature.update_largest_mass.assert_not_called()

virtual_ecosystem/models/animal/animal_cohorts.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ def __init__(
109109
self.reproductive_mass_cnp = CNP(0.0, 0.0, 0.0)
110110
"""The reproductive mass of each stoichiometric element found in the animal
111111
cohort, {"carbon": value, "nitrogen": value, "phosphorus": value}."""
112+
self.largest_mass_achieved: float = mass
113+
"""The largest body-mass ever achieved by this cohort [kg]."""
112114

113115
@property
114116
def mass_current(self) -> float:
@@ -120,6 +122,19 @@ def reproductive_mass(self) -> float:
120122
"""Dynamically calculate the current reproductive mass from CNP object."""
121123
return self.reproductive_mass_cnp.total
122124

125+
def update_largest_mass(self) -> None:
126+
"""Update the record of the largest body-mass achieved by this cohort.
127+
128+
This provides a rough approximation of the development process. Once maturity
129+
is achieved, adult mass becomes the reference for starvation as normal.
130+
131+
"""
132+
133+
if self.mass_current > self.largest_mass_achieved:
134+
self.largest_mass_achieved = min(
135+
self.mass_current, self.functional_group.adult_mass
136+
)
137+
123138
def get_territory_cells(self, centroid_key: int) -> list[int]:
124139
"""This calls bfs_territory to determine the scope of the territory.
125140
@@ -1472,7 +1487,7 @@ def inflict_non_predation_mortality(
14721487

14731488
t_to_maturity = self.time_to_maturity
14741489
t_since_maturity = self.time_since_maturity
1475-
mass_max = self.functional_group.adult_mass # this might not be only solution
1490+
mass_max = self.largest_mass_achieved # growth to adult_mass
14761491

14771492
u_bg = sf.background_mortality(
14781493
self.constants.u_bg

virtual_ecosystem/models/animal/animal_model.py

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -363,9 +363,6 @@ def _update(self, time_index: int, **kwargs: Any) -> None:
363363
events would be simultaneous. The ordering within the method is less a question
364364
of the science and more a question of computational logic and stability.
365365
366-
TODO: update so that it just cycles through the community methods, each of those
367-
will cycle through all cohorts in the model
368-
369366
Args:
370367
time_index: The index representing the current time step in the data object.
371368
**kwargs: Further arguments to the update method.
@@ -382,10 +379,8 @@ def _update(self, time_index: int, **kwargs: Any) -> None:
382379
self.migrate_external_community()
383380
self.metabolize_community(self.update_interval_timedelta)
384381
self.inflict_non_predation_mortality_community(self.update_interval_timedelta)
385-
self.update_migrated_and_aquatic(self.update_interval_timedelta)
386-
self.reintegrate_community()
387-
self.remove_dead_cohort_community()
388-
self.increase_age_community(self.update_interval_timedelta)
382+
self.update_community_bookkeeping(self.update_interval_timedelta)
383+
self.update_cohort_bookkeeping(self.update_interval_timedelta)
389384

390385
# Now that communities have been updated information required to update the
391386
# soil and litter models can be extracted
@@ -401,6 +396,35 @@ def _update(self, time_index: int, **kwargs: Any) -> None:
401396
# Update population densities
402397
self.update_population_densities()
403398

399+
def update_community_bookkeeping(self, dt: timedelta64) -> None:
400+
"""Perform status updates and cleanup at the community level.
401+
402+
This includes:
403+
- Updating timers for migrated or aquatic cohorts
404+
- Reintegration of previously inactive cohorts
405+
- Removal of dead cohorts
406+
407+
Args:
408+
dt: Time step duration [days].
409+
"""
410+
411+
self.update_migrated_and_aquatic(dt)
412+
self.reintegrate_community()
413+
self.remove_dead_cohort_community()
414+
415+
def update_cohort_bookkeeping(self, dt: timedelta64) -> None:
416+
"""Perform lifecycle-related updates for each cohort.
417+
418+
This includes:
419+
- Increasing age
420+
- Updating largest mass achieved
421+
422+
Args:
423+
dt: Time step duration [days].
424+
"""
425+
self.increase_age_community(dt)
426+
self.handle_ontogeny()
427+
404428
def cleanup(self) -> None:
405429
"""Placeholder function for animal model cleanup."""
406430

@@ -1139,6 +1163,16 @@ def increase_age_community(self, dt: timedelta64) -> None:
11391163
for cohort in self.active_cohorts.values():
11401164
cohort.increase_age(dt)
11411165

1166+
def handle_ontogeny(self) -> None:
1167+
"""Update largest body mass achieved for immature cohorts.
1168+
1169+
This is used to support ontogeny-aware starvation calculations.
1170+
"""
1171+
1172+
for cohort in self.active_cohorts.values():
1173+
if not cohort.is_mature:
1174+
cohort.update_largest_mass()
1175+
11421176
def inflict_non_predation_mortality_community(self, dt: timedelta64) -> None:
11431177
"""This handles natural mortality for all cohorts in a community.
11441178

0 commit comments

Comments
 (0)