Skip to content

Draft for plant resources rework#1015

Closed
TaranRallings wants to merge 10 commits intodevelopfrom
feature/plant-piping
Closed

Draft for plant resources rework#1015
TaranRallings wants to merge 10 commits intodevelopfrom
feature/plant-piping

Conversation

@TaranRallings
Copy link
Copy Markdown
Collaborator

@TaranRallings TaranRallings commented Aug 29, 2025

Description

This is a draft of a new plant resources interface for sitting inside the animal model. I would like it to do a few things:

  • create one plant resource object per resource type per grid cell (following the logic of the SoilPool class)
  • pool mass between pfts where appropriate
  • interface with foraging
  • communicate mass information with Data

I've tried fiddling with the data import stuff on my own but I think it will be much more efficient if you can help me out on that front. I have no problem doing all of the foraging interface but the plant data end is pretty arcane for me.

Can you provide feedback on:

  • overall form (does the use of one class for these multiple resources make sense?)
  • how to construct the _extract_mass_from_data method (or a better solution for the same problem)

Thanks!

Fixes # (issue)

Type of change

  • New feature (non-breaking change which adds functionality)
  • Optimization (back-end change that speeds up the code)
  • Bug fix (non-breaking change which fixes an issue)

Key checklist

  • Make sure you've run the pre-commit checks: $ pre-commit run -a
  • All tests pass: $ poetry run pytest

Further checks

  • Code is commented, particularly in hard-to-understand areas
  • Tests added that prove fix is effective or that feature works
  • Relevant documentation reviewed and updated

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Aug 29, 2025

Codecov Report

❌ Patch coverage is 0% with 159 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.48%. Comparing base (4dd1336) to head (647284e).

Files with missing lines Patch % Lines
...tual_ecosystem/models/animal/plant_resources_v3.py 0.00% 108 Missing ⚠️
...tual_ecosystem/models/animal/plant_resources_v2.py 0.00% 51 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #1015      +/-   ##
===========================================
- Coverage    94.60%   92.48%   -2.12%     
===========================================
  Files           79       81       +2     
  Lines         6937     7096     +159     
===========================================
  Hits          6563     6563              
- Misses         374      533     +159     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Collaborator

@davidorme davidorme left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've run through the sketch in v2 and tried to unpack it into a working model of a draft API in plant_resources_v3.py. If I understand correctly, we need to:

  • Identify a set of variables in data that provide a resource mass and some metric of elemental composition (masses or ratios - we could probably use either).
  • Identify a variable used to track consumed mass from that resource.
  • The resource is always structured by cell_id but may also be structured vertically and by PFT. We probably need to keep PFTs as separate resources.
  • We then need a mechanism by which an array can be parcelled out as individual cells using something that supports the Resource protocol.
  • We then need to be able to pull consumed mass back across cells and write it back to data.

The file I've pushed hinges on two key mechanisms:

  1. The ArrayResources class can be used to produce cell specific resources using indexing: so plant_resource_0 = array_resource_instance[0] gives a PlantResource for cell 0.
  2. The total mass consumed in plant_resource_0 is a sneaky view back into an array across cells in the originating ArrayResources instance - so the total mass consumed is auto compiled.

The file contains some code at the end showing a rough workflow for the new classes. I think this should drop into your existing framework relatively easily?

@jacobcook1995 How does this compare to what is being used for soil array resources - can we unify to a common standard here?

@dalonsoa Could you have a quick look at the logic here - I think using array views to track the data generated by the child PlantResource back into the parent ArrayResources seems sensible but are there better ways to achieve this?

Comment on lines +66 to +70
self.mass_stoich: dict[str, float] = {
"carbon": 0.0,
"nitrogen": 0.0,
"phosphorus": 0.0,
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to track this? The get_eaten() method tracks off mass_current and the cnp_proportions - it doesn't seem like these masses need to be tracked separately (unless it is used for targeting resources, but I would think that animals target resources based on their expectations of stochiometry, rather than rather god-like knowledge of the total pool).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A dictionary of this kind is just the standard format of passing mass around in the animal model. The way I did CNP in the plant get_eaten is a non-standard implementation. I wasn't sure how you were handling CNP exactly so I built in the bits required for either route. Ultimately, as long as get_eaten returns a dict of that kind, it should work for the animal model.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure how you were handling CNP exactly so I built in the bits required for either route.

Neither am I, yet 😄

However it comes in (mass or ratio) I think handling it internally as proportions and only tracking total mass is easier. It's not like the animals can alter the ratios? 🤞

@TaranRallings
Copy link
Copy Markdown
Collaborator Author

This looks like a very promising way to break the flow down! I'll leave fiddling with foraging interface for after we hear from Jacob and Diego.

Comment on lines +75 to +76
self.n_ratio_var = n_ratio_var
self.p_ratio_var = p_ratio_var
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we name these CN & CP ratios just for continuity?

@davidorme
Copy link
Copy Markdown
Collaborator

@TaranRallings Do you prefer to deal with masses of C, N, P or - as is the case in the code at the moment - with carbon mass and then CN and CP ratios? @sallymatson and I want to standardise how we write C+N+P to the Data object on either masses (which we mildly prefer) or ratios.

@jacobcook1995
Copy link
Copy Markdown
Collaborator

So the above could definitely work for the litter pools, and seems like a much cleaner approach. I think it also could work for the soil pom pool.

With some basic modification I think it could work for the soil bacteria pool (the issue there is that the stoichiometry comes is a constant rather than being recorded in the data object).

Getting it to work for the soil fungi pool would be much more tricky, because in that case you have to combine three fungal pools into a single animal available pool (+ weighting the stoichiometric ratios appropriately)

Fungal fruiting bodies would need a pretty big refactor to work in this framework (because they are primarily updated in the animal model, which could change but that would require thought). Carcasses and excrement would have a similar problem I think

Given all that I'm not sure what the best approach is. I think it definitely makes sense to treat litter pools as plant resources, that follow the same framework. But I'm unsure whether it makes more sense to try and fit the other pools within the same overarching framework, or just to treat them as something meaningfully different that is handled differently?

Copy link
Copy Markdown
Collaborator

@dalonsoa dalonsoa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the slow reply - I hadn't spotted this.

The approach looks quite sensible and I do not see any obvious improvement. I have a couple of comments, though, because I'm not entirely sure where this would be used.

Comment on lines +82 to +86
for var in (mass_var, n_ratio_var, p_ratio_var, mass_consumed_var):
if var not in data:
raise ValueError(
f"Cannot initialise ArrayResource: {var} not found in data object"
)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be needed. This resource would be initialised by a model - presumably the AnimalModel - which will have among its required variables (possibly required by init ones?) the ones indicated here. So, by this point, all this validation should have been done already.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True - there's a point at which putting belt and braces of error checking on all your code makes it slow down too much. We could lose this - any crash is then a result of the code of the AnimalModel being wrong - it isn't a user facing calibration.

Comment on lines +312 to +344
# Then within a single time loop
for cell_id in np.arange(12):
# Get the cell level resource for this cell from each array resource
cell_resources = [array_res[cell_id] for array_res in herbivory_resources]

# All consumers feed on all four resources
for herbivore in [consumer] * 4:
for resource in cell_resources:
resource.get_eaten(consumed_mass=rng.random(), consumer=herbivore)

# After the loop finishes print out the accumulated herbivory in the ArrayResource
# instances
for array_res in herbivory_resources:
print(array_res)
print(array_res.consumed_mass)

# The data object still doesn't know about herbivory
data["leaf_mass_consumed"]
data["subcanopy_mass_consumed"]

# So use the write_herbivory() to push the accumualate value out to the Data object.
# Note that the PFT specific resources are writing to different parts of the same data
# array.
for array_res in herbivory_resources:
array_res.write_herbivory()

data["leaf_mass_consumed"]
data["subcanopy_mass_consumed"]

# And then, before starting the next loop:

for array_res in herbivory_resources:
array_res.set_mass_and_elemental_ratios()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where would all of these be happening? Within the AnimalModel update or somewhere else?

Also, that triply nested for loop looks super inefficient. although it is not obvious how to improve it as none of the functionality is vectorised...

Copy link
Copy Markdown
Collaborator

@davidorme davidorme Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From this line down, it would be within AnimalModel._update (although probably as a call to an encapsulating method?):

# Get a tuple of all herbivory array resources

Everything above that is the PlantsModel pushing Plant data into the Data object, in PlantsModel.__init__ and PlantsModel._update

And I agree the triply nested loop looks bad but we'd need to vectorise the animal model to avoid it.

mass_consumed_var="subcanopy_seedbank_biomass_consumed",
vertical_occupancy=VerticalOccupancy.GROUND,
),
)
Copy link
Copy Markdown
Collaborator

@davidorme davidorme Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably do this - should be easier to maintain?

Suggested change
)
# TODO: plant resource
plant_resources = [
(
"subcanopy_vegetation_biomass",
"subcanopy_vegetation_n_ratio",
"subcanopy_vegetation_p_ratio",
"subcanopy_vegetation_biomass_consumed",
VerticalOccupancy.GROUND,
),
(
"subcanopy_seedbank_biomass",
"subcanopy_seedbank_n_ratio",
"subcanopy_seedbank_p_ratio",
"subcanopy_seedbank_biomass_consumed",
VerticalOccupancy.GROUND,
),
]
self._self._plant_array_resources = (
ArrayResources(
data=self.data,
mass_var=mvar,
n_ratio_var=nvar,
p_ratio_var=pvar,
mass_consumed_var=mcvar,
vertical_occupancy=vocc
) for mvar, nvar, pvar, mcvar, vocc in plant_resources
)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, that's much better!

This was referenced Feb 5, 2026
@davidorme
Copy link
Copy Markdown
Collaborator

I'm closing this as it is superseded by #1318

@davidorme davidorme closed this Feb 25, 2026
@davidorme davidorme deleted the feature/plant-piping branch February 25, 2026 11:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants