Repository navigation
Conversation
…libration-validation
…libration-validation
…alidation' of https://github.com/ImperialCollegeLondon/virtual_rainforest into 1176-write-out-daily-hydrology-values-for-calibration-validation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1949 +/- ##
==========================================
- Coverage 89.79% 89.78% -0.01%
==========================================
Files 83 83
Lines 9678 9708 +30
==========================================
+ Hits 8690 8716 +26
- Misses 988 992 +4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
David Orme (davidorme)
left a comment
There was a problem hiding this comment.
vgro, some questions - some things I don't understand.
| dims: tuple[Hashable, ...] | ||
| if values_out.ndim == 1: | ||
| dims = ["cell_id"] | ||
| dims = ("cell_id",) | ||
| elif values_out.ndim == 2: | ||
| dims = ["layers", "cell_id"] | ||
| dims = ("layers", "cell_id") | ||
| else: | ||
| raise ValueError( | ||
| f"Unsupported dimensions for variable '{var}': {values.shape}" | ||
| ) | ||
|
|
||
| output[var] = DataArray(values_out, dims=dims) | ||
|
|
||
| for var in hourly_vars: | ||
| if var in data_record: | ||
| values = np.asarray(data_record[var]) | ||
| hourly_dims: tuple[Hashable, ...] | ||
| if values.ndim == 2: | ||
| hourly_dims = ("hour", "cell_id") | ||
| elif values.ndim == 3: | ||
| hourly_dims = ("hour", "layers", "cell_id") | ||
| else: | ||
| raise ValueError( | ||
| f"Unsupported hourly dimensions for variable '{var}': " | ||
| f"{values.shape}" | ||
| ) | ||
| elif var in output: | ||
| n_hours = time_dim | ||
| if n_hours is None: | ||
| if not data_record: | ||
| raise ValueError( | ||
| f"Cannot determine the hour dimension for static output'{var}'." | ||
| ) | ||
| n_hours = np.asarray(next(iter(data_record.values()))).shape[0] | ||
| values = np.broadcast_to( | ||
| output[var].to_numpy(), | ||
| (n_hours, *output[var].shape), | ||
| ).copy() | ||
| hourly_dims = ("hour", *output[var].dims) | ||
| else: | ||
| raise ValueError( | ||
| f"Hourly output requested for unavailable variable '{var}'." | ||
| ) | ||
|
|
||
| layer_template = layer_structure.from_template() | ||
| coords: dict[Hashable, Any] = { | ||
| "hour": np.arange(values.shape[0]), | ||
| "cell_id": layer_template["cell_id"], | ||
| } | ||
| if "layers" in hourly_dims: | ||
| n_layers = values.shape[hourly_dims.index("layers")] | ||
| if n_layers == layer_template.sizes["layers"]: | ||
| coords["layers"] = layer_template["layers"] | ||
| coords["layer_roles"] = ( | ||
| "layers", | ||
| layer_template["layer_roles"].to_numpy(), | ||
| ) | ||
| else: | ||
| coords["layers"] = np.arange(n_layers) | ||
| output[f"{var}_hourly"] = DataArray(values, dims=hourly_dims, coords=coords) | ||
|
|
There was a problem hiding this comment.
I can't quite follow what the outputs are here. I think it would be faster and more efficient here to define a set of specific templates at _setup and then map variable names onto those templates.
It is hard though to see how many templates we're talking about here. It seems like four?
dims = ("cell_id",)
dims = ("layers", "cell_id")
hourly_dims = ("hour", "cell_id")
hourly_dims = ("hour", "layers", "cell_id")
But then there are ones with fewer layers?
There was a problem hiding this comment.
Probably yes, this is not a new problem but so far I have hacked around it. I'll look into it and see what makes sense. Just to confirm, do you mean the layer_structure.from_template()?
There was a problem hiding this comment.
So I think you can start with a maximal set of coordinates. And then there is a long but limited set of possible outputs for the model where we know what coordinates are used. So, you could use the maximal set of coords to define a smaller number of specific templates, or you could define which coords are required (or easier, not required) for each variable. The reduced coordinates object then defines the expected shape of the data. The code below is a sketch that defines the dims per variable, removes most of the heavy lifting of coordinate juggling - still not sure about the reduced layers though.
# Define the most complex shape
all_coords = layer_template.coords.assign(hours=np.arange(24))
# Define unrequired coords
unused_dims = {
"var_name_1": ("hours", "layers",),
"var_name_1_hourly": ("layers",),
}
# Build outputs
outputs = {}
for var, data in vars:
try:
outputs[var] = DataArray(data, coords=all_coords.drop_dims(unused_dims[var]))
except xr.CoordinateValidationError:
raise ValueError(f"Unexpected data shape with {var}: {data.shape} not {tuple(c.dims.values())}")There was a problem hiding this comment.
I'm not sure about the best specific implementation, but I agree with David Orme (@davidorme) that everything that could be offloaded to the setup because it is information already known, so it is run only once, will be a gain. In the end, all these nested conditionals and loops will slow down the execution unnecessary. Besides, the code will be shorter and simpler (this method is already in the long side).
Diego Alonso Álvarez (dalonsoa)
left a comment
There was a problem hiding this comment.
I mostly agree with David's comments about trying to do some templating upfront. Also, some functions are becoming a bit large, so when possible I would split them into smaller chunks.
| dims: tuple[Hashable, ...] | ||
| if values_out.ndim == 1: | ||
| dims = ["cell_id"] | ||
| dims = ("cell_id",) | ||
| elif values_out.ndim == 2: | ||
| dims = ["layers", "cell_id"] | ||
| dims = ("layers", "cell_id") | ||
| else: | ||
| raise ValueError( | ||
| f"Unsupported dimensions for variable '{var}': {values.shape}" | ||
| ) | ||
|
|
||
| output[var] = DataArray(values_out, dims=dims) | ||
|
|
||
| for var in hourly_vars: | ||
| if var in data_record: | ||
| values = np.asarray(data_record[var]) | ||
| hourly_dims: tuple[Hashable, ...] | ||
| if values.ndim == 2: | ||
| hourly_dims = ("hour", "cell_id") | ||
| elif values.ndim == 3: | ||
| hourly_dims = ("hour", "layers", "cell_id") | ||
| else: | ||
| raise ValueError( | ||
| f"Unsupported hourly dimensions for variable '{var}': " | ||
| f"{values.shape}" | ||
| ) | ||
| elif var in output: | ||
| n_hours = time_dim | ||
| if n_hours is None: | ||
| if not data_record: | ||
| raise ValueError( | ||
| f"Cannot determine the hour dimension for static output'{var}'." | ||
| ) | ||
| n_hours = np.asarray(next(iter(data_record.values()))).shape[0] | ||
| values = np.broadcast_to( | ||
| output[var].to_numpy(), | ||
| (n_hours, *output[var].shape), | ||
| ).copy() | ||
| hourly_dims = ("hour", *output[var].dims) | ||
| else: | ||
| raise ValueError( | ||
| f"Hourly output requested for unavailable variable '{var}'." | ||
| ) | ||
|
|
||
| layer_template = layer_structure.from_template() | ||
| coords: dict[Hashable, Any] = { | ||
| "hour": np.arange(values.shape[0]), | ||
| "cell_id": layer_template["cell_id"], | ||
| } | ||
| if "layers" in hourly_dims: | ||
| n_layers = values.shape[hourly_dims.index("layers")] | ||
| if n_layers == layer_template.sizes["layers"]: | ||
| coords["layers"] = layer_template["layers"] | ||
| coords["layer_roles"] = ( | ||
| "layers", | ||
| layer_template["layer_roles"].to_numpy(), | ||
| ) | ||
| else: | ||
| coords["layers"] = np.arange(n_layers) | ||
| output[f"{var}_hourly"] = DataArray(values, dims=hourly_dims, coords=coords) | ||
|
|
There was a problem hiding this comment.
I'm not sure about the best specific implementation, but I agree with David Orme (@davidorme) that everything that could be offloaded to the setup because it is information already known, so it is run only once, will be a gain. In the end, all these nested conditionals and loops will slow down the execution unnecessary. Besides, the code will be shorter and simpler (this method is already in the long side).
| daily_lists=daily_lists, | ||
| water_balance_fluxes=water_balance_fluxes, | ||
| daily_diagnostics=daily_diagnostics, | ||
| for name, values in daily_lists.items(): |
There was a problem hiding this comment.
Do the dimensions and coordinates of these change every iteration? Because if they do not, you could defined them upfront (in the setup or in the first iteration, and then cache the result), and use them in every subsequent calls. It might not save much time, but will make the code much simpler, just populating the templates for the given variable. Much like pointed out for the hourly case.
There was a problem hiding this comment.
Yup - I agree. Creating template arrays in setup along with the mapping of var to template should make it compact (apart from the mapping) and clean.
There was a problem hiding this comment.
The plants model has a similar thing in _setup - some of these could even be moved into core if enough models use them, but the hour and day dims are going to be local templates.
virtual_ecosystem/virtual_ecosystem/models/plants/plants_model.py
Lines 426 to 449 in f99e14e
| variable_type = "float" | ||
|
|
||
| [[variable]] | ||
| axis = ["spatial"] |
There was a problem hiding this comment.
I'm I bit unsure about the approach here, but these variables will have another dimension, even if it is not validated anywhere. I think it should be disclosed here, if just as a sort of documentation.
There was a problem hiding this comment.
Yeah - this is a longstanding lag in the documentation/internal. The axis data is currently woefully incomplete and we currently don't have validation on the non-spatial axes.
There was a problem hiding this comment.
I agree this should be declared here, but it does not work with the current setup, I am getting pydantic ValueErrors if I add a new dimension. I am not very familiar with the BaseModel and the axis validation, is this something to hold or can I include this in the core easily?
There was a problem hiding this comment.
This is definitely a different PR. Convert them to comments in the TOML if you want to capture the data, but otherwise this is a much bigger fix than this PR.
This PR adds daily values from the hydrology model andhourly values from the abiotic model to the data object. I introduced new dimensions for these additional variables and saved then with the suffix
_dailyor_hourlyfor each relevant variable.The hydrology csv diagnostic option is removed.
I made a small change to the diurnal temperature range, too, this needs further investigation, see #1950.
Fixes #1176
Type of change
Key checklist
pre-commitchecks:$ pre-commit run -a$ poetry run pytestFurther checks
data science team included as reviewers? -> NA