-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Minimal carbon pool model #134
Conversation
…g for status to be reported' problem
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks really good and it's also a good example of how a PR should be done (i.e. you provided a good explanation, and your code has tests and lots of docstrings). Well done!
I've made some small suggestions throughout. Some slightly "bigger picture suggestions I had were:
- It'd be helpful to say what units each of your variables are throughout (in docstrings etc.) where they represent physical values (e.g. I'd assume all your temperatures are in °C but would like to know for sure, I don't know what the unit of soil moisture is at all etc.)
- While it's great that you have tests for each of your functions, you're oftentimes only testing that it works for a single set of parameters. This is much better than having nothing at all, but I think you should go one step further and test multiple sets or, ideally, ranges of values (i.e. by using
@pytest.mark.parametrize
). I was working on something similar myself today: https://github.com/ImperialCollegeLondon/FINESSE/blob/main/tests/test_tc4820.py
I realise I forgot to answer your questions:
|
On question 2, I think this overlaps with #135 - I've added a sketch of how this might work across modules to that discussion. |
Codecov Report
@@ Coverage Diff @@
## develop #134 +/- ##
===========================================
+ Coverage 94.95% 95.31% +0.35%
===========================================
Files 14 15 +1
Lines 714 768 +54
===========================================
+ Hits 678 732 +54
Misses 36 36
📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good - some suggestions on the tests and some possible tweaks on the implementation, but nothing serious. I think the tests are worth updating, so requesting those changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good but a few things to look at.
Making sure latest doc changes are in my dummy carbon branch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've made some very minor comments, but otherwise I think this is good to go. Good job!
labile carbon (m^3 kg^-1) | ||
""" | ||
|
||
return 10.0 ** (BINDING_WITH_PH["slope"] * pH + BINDING_WITH_PH["intercept"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't remember if I said this in my original review, but I think BINDING_WITH_PH
would be better as a dataclass
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh right is that the case for everywhere that I'm using dictionaries of constants? Or is this specific to BINDING_WITH_PH
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And the class would be defined like this
@dataclass
class BindingWithPH:
"""From linear regression (Mayes et al. (2012))."""
intercept: float = -0.186
slope: float = -0.216
and accessed like this?
return 10.0 ** (BindingWithPH.slope * pH + BindingWithPH.intercept)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it is just the object.thing
notation then there are other options for having dot
style dictionaries (dotmap
for example). But @alexdewar did you mean that BindingWithPH
would also provide BindingWithPH.calc_binding
?
@dataclass
class BindingWithPH:
"""From linear regression (Mayes et al. (2012))."""
intercept: float = -0.186
slope: float = -0.216
def calc_binding(ph: NDArray) -> NDArray
return 10.0 ** (self.slope * pH + self.intercept)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More generally, I think there's an argument for the constants
module providing something that can be easily serialised to JSON or YAML so that the constants
can become part of the configuration if needed. core.constants
might provide a default set, but people might want to play around with the settings.
I've used dataclasses
for this kind of role here: https://github.com/davidorme/pyrealm/blob/master/pyrealm/param_classes.py
I will accept that the size of the dataclasses may not be ideal 😬
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jacobcook1995 Yes, that's essentially what I had in mind, but I'm open to suggestions if something like dotmap
(which I hadn't heard of till now) is better.
The reason I suggested a dataclass was so you can get better type linting (i.e. mypy
will be able to see what fields your BindingWithPH
class has and what types they are). You could also just have them as separate variables.
Made the decision to switch to data classes rather than dot map as I could work out how to add parameter specific docstrings more easily with data classes. Happy for this to change down the line when we agree upon a consistent approach between modules for constants units etc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Obvious caveats about how we are going to do constants and possibly having some kind of shared utility for bounds checking and if so where.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait. I think there's something off about using dataclasses like that.
So I may be not understanding how dataclasses are used, but I've always seen them used to create an instance of the dataclass. So given: @dataclass
class BindingWithPH:
"""From linear regression (Mayes et al. (2012))."""
intercept: float = -0.186
slope: float = -0.216 Then a function might work like this: def calculate_binding_coefficient(pH: NDArray[np.float32], coef: BindingWithPH = BindingWithPH()) And then the code can use res = calculate_binding_coefficient(pH_array, BindingWithPH(slope=-0.23)) Here, we're using the attributes more like class attributes. The dataclass does expose those attributes but they seem to fundamentally not do what I expect. So for example: In [40]: @dataclass
...: class BindingWithPH:
...: """From linear regression (Mayes et al. (2012))."""
...: intercept: float = -0.186
...: slope: float = -0.216
...:
# create a normal instance
In [41]: inst = BindingWithPH()
In [42]: inst
Out[42]: BindingWithPH(intercept=-0.186, slope=-0.216)
# and with a different slope
In [43]: inst = BindingWithPH(slope=3)
In [44]: inst
Out[44]: BindingWithPH(intercept=-0.186, slope=3)
# The value _can_ be accessed directly from the class, not an instance
# as if it is a class attribute
In [45]: BindingWithPH.slope
Out[45]: -0.216
# And it can be changed too
In [46]: BindingWithPH.slope = 3
In [47]: BindingWithPH.slope
Out[47]: 3
# BUT that doesn't interact with the instance __init__, which keeps
# returning the original defaults.
In [48]: inst = BindingWithPH()
In [49]: inst
Out[49]: BindingWithPH(intercept=-0.186, slope=-0.216)
In [50]: BindingWithPH.__init__
Out[50]: <function __main__.BindingWithPH.__init__(self, intercept: float = -0.186, slope: float = -0.216) -> None> I'm not sure what the advantage of I'm also wary of how users change these values. I'm not sure, but at the moment I think you could do it by changing the "class attribute" value as above, but you then have absolutely no control over what other functions might be accessing that altered value. If an instance is created, with defaults or altered, then the user knows what version they are passing. This all seems really familiar for some reason! |
Yeah, I guess a I hear your concerns about the values being mutable, but that is kind of what you get with Python. Module-level constants are mutable too. |
I think |
So the suggestion here @davidorme would be to keep the classes as they are but move them to a |
Yes - I think so. That's very close to what you have already and seems like a sensible way to organise things anyway. Something else might come of #162 but no alarm bells yet! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
Description
I've added a basic carbon pool to the soil module. It only includes 2 pools (the full model will probably include 5 pools), and a function that handles transfers between these two pools. This is all bundled together as part of a
SoilCarbon
class.I was originally intending to implement an alternate equation form and a method to switch between the two, but decided that was a bit ambitious for a single pull request. Structurally there's a couple of things that I'm not completely happy with and would want to clear up before I moved onto something like that.
No hurry on reviewing this, I mainly wanted to get the pull request submitted before I finished for Christmas.
Fixes #99
Type of change
Key checklist
pre-commit
checks:$ pre-commit run -a
$ poetry run pytest
Further checks