-
Notifications
You must be signed in to change notification settings - Fork 15
Spatially varying canopy height #1502
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
base: main
Are you sure you want to change the base?
Conversation
Previously h_leaf was hardcoded to FT(1), now it uses toml_dict["canopy_height"] to ensure consistency between biomass and hydraulics height parameters.
…acts - Reads spatially varying canopy height from CLM vegetation_properties_map.nc - Returns ClimaCore Field with height values for each grid cell - Follows same pattern as clm_rooting_depth() and clm_medlyn_g1() - Heights are based on dominant PFT from CLM5 MONTHLY_HEIGHT_TOP data
CLM data uses 'z_top' (Canopy top height) not 'canopy_height'. Data investigation findings: - 192x288 grid (0.9x1.25 degrees) - Height range: 0-40.13m - 92.5% < 1m (grasslands/crops) - 7.4% >= 10m (4068 cells, tall forests) - Max height: 40.13m (tropical forests) - Median: 0m (sparse/no vegetation dominant)
- Add HTH type parameter to support Union{FT, Field} for height
- Update struct definition in biomass.jl to accept both scalar and Field height
- Update outer constructor to pass height type to struct instantiation
- Update Canopy.jl constructor to include height type parameter
- This allows height to be either a scalar (backward compatible) or a Field (new functionality)
- Implement effective_canopy_height() to cap heights below atmospheric reference - Default buffer of 2m ensures adequate space for flux calculations - Logs warning with statistics when capping occurs - Addresses constraint that ERA5 forcing is at 10m reference height - Prevents numerical issues from canopy extending above atmospheric layer
- Import clm_canopy_height and effective_canopy_height functions - Read canopy height from CLM artifact data in snowy_land_pmodel.jl - Apply capping to 8m (10m atmospheric height - 2m buffer) - Pass spatially-varying height to PrescribedBiomassModel - This replaces the previous fixed 1m canopy height with realistic CLM values
Tests added: 1. spatial_parameters.jl: - Test clm_canopy_height() reads CLM data correctly - Test effective_canopy_height() capping with default and custom buffers - Verify proper field structure and value constraints 2. test_spatially_varying_canopy_height.jl (new file): - Test PrescribedBiomassModel with scalar height (backward compatibility) - Test PrescribedBiomassModel with Field height (new functionality) - Test integration with CLM height data and capping - Test full CanopyModel construction with spatially-varying height All tests verify: - Correct Field axes and types - Height constraints (capping below atmospheric reference) - Non-negative heights - Backward compatibility with scalar heights
Create experiment script to compare: - Run 1: Default configuration with height=1m everywhere - Run 2: Spatially-varying height from CLM data (capped at 8m) Both runs use same 1-year period (2008-03-01 to 2009-03-01) to enable direct comparison of model outputs and assess impact of spatially-varying canopy height on simulated fluxes and states.
- Replace count() with ifelse() to avoid Boolean Fields on GPU - Use float mask (1.0/0.0) instead of boolean for counting - Fixes 'Bool cannot be represented using Float64' error on CUDA
- Remove Int() conversion that caused overflow with large GPU arrays - Keep n_capped_sum as Float for display - Use round() for cleaner output
…t_height() - Changed FT(0.67) * height to FT(0.67) .* height - Fixes MethodError when height is a Field (spatially-varying) - Maintains backward compatibility with scalar height
- Changed biomass.height == hydraulics_height to use all(isapprox.(...)) - Original == with Field was creating Field of booleans (very slow) - Maintains backward compatibility with scalar height using ≈ - This was causing 3x+ slowdown in spatially-varying height runs
These simulation outputs have been moved to ~/climaland_outputs/ for local analysis and are not part of the core feature implementation.
Wrap parent() array accesses with Array() to convert from GPU to CPU arrays before scalar indexing. This prevents 'Scalar indexing is disallowed' errors when running tests on GPU.
| """ | ||
| function ClimaLand.displacement_height(model::CanopyModel{FT}, Y, p) where {FT} | ||
| return FT(0.67) * model.biomass.height | ||
| return FT(0.67) .* model.biomass.height |
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.
Any operations like:
a .* b will allocate, so we should be careful to not do this in functions that are evaluated each timestep (it is OK if we do it selectively in initialization).
Here maybe we can do:
@. lazy( FT(0.67) * model.biomass.height)
which will only execute the operation when it is part of a larger set of operations updating something in place (no allocation if in place)
| isapprox.(biomass.height, hydraulics_height, rtol = 1e-10), | ||
| ) "Biomass height Field must match hydraulics height" | ||
| else | ||
| @assert biomass.height ≈ hydraulics_height "Biomass height must match hydraulics height" |
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 dont quite understand how this works, because the hydraulics_height is a scalar. How can this pass when biomass.height is spatialy varying?
In general we need to go through src and make sure these are consistent, because in some places we use the 'hydraulics" height as the height
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.
It would be good to document what biomass height means and what hydraulics height means, and what they're each used for
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.
You're right - the hydraulics compartments are always scalar (1m). When biomass.height varies spatially, only the aerodynamic calculations use those varying heights. The hydraulics structure stays uniform everywhere.
This is a current limitation - we can extend hydraulics to support spatial variation in a follow-up PR.
| effective_canopy_height(raw_canopy_height, FT(10.0); buffer = FT(2.0)) | ||
|
|
||
| # Create biomass model with spatially-varying height | ||
| biomass = PrescribedBiomassModel{FT}( |
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 think it would be good to add lines 112-116 into the PrescribedBiomassModel constructor, so it is not something that has to be updated each time (if we decide to include this as the default)
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.
let's try running this and see how the SHF, LHF, etc change compared to observations
Moving forwards, I think there are two ways this can be extended:
- make a canopy roughness model type. right now we are using physical quantities (height) to compute roughness and displacement height (using physical equations someone has come up with). Something more flexible would be to predict roughness, displacement height, from inputs like LAI, canopy height, etc using a data driven model. Having a type for "canopy roughness" would let us switch between the two easily. (I am separately adding this already in another PR)
- in Biomass - create a type of "height model": this would be prescribed (constant or spatially varying) for now, PrescribedCanopyHeight. Then in the future we can also easily make different parameterizations for canopy height (e.g. prognostic, predicting it from input/time varying data). but this seems very far in the future compared to the first point
| # Create a spatially-varying height field | ||
| coords = ClimaCore.Fields.coordinate_field(surface_space) | ||
| field_height = coords.lat .* 0 .+ FT(3.0) # Constant field of 3m for testing |
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 is a little simpler, but what you have is fine too
| # Create a spatially-varying height field | |
| coords = ClimaCore.Fields.coordinate_field(surface_space) | |
| field_height = coords.lat .* 0 .+ FT(3.0) # Constant field of 3m for testing | |
| # Create a spatially-varying height field | |
| field_height = zeros(surface_space) .+ FT(3) # Constant field of 3m for testing |
| isapprox.(biomass.height, hydraulics_height, rtol = 1e-10), | ||
| ) "Biomass height Field must match hydraulics height" | ||
| else | ||
| @assert biomass.height ≈ hydraulics_height "Biomass height must match hydraulics height" |
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.
It would be good to document what biomass height means and what hydraulics height means, and what they're each used for
| canopy_height = SpaceVaryingInput( | ||
| joinpath(clm_artifact_path, "vegetation_properties_map.nc"), | ||
| "z_top", | ||
| surface_space; | ||
| regridder_type, | ||
| regridder_kwargs = (; extrapolation_bc, interpolation_method), | ||
| ) |
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.
We could provide the effective_canopy_height as a preprocessing function instead, so we don't have to think about it wherever we access the canopy height data. This would be safer because we'll avoid users forgetting to apply the clipping.
file_reader_kwargs = (; preprocess_func = (data) -> effective_canopy_height(data, z_atm; buffer),),
And then we take z_atm and buffer as inputs to this function
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.
effective_canopy_height would also have to change to take in a float canopy_height instead of the whole field, since here it'll be broadcasted over the field. We wouldn't be able to log the information you have in the warning, but I think that's okay. Usually when we add warnings like this we regret it later because it clutters the simulation output
| @@ -0,0 +1,187 @@ | |||
| # RUN 2 ONLY: Spatially-Varying Canopy Height | |||
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 this is going to be merged into main it would be good to rename to something like snowy_land_varying_height.jl
| # module load climacommon | ||
| # export CLIMACOMMS_DEVICE=CUDA | ||
| # export CLIMACOMMS_CONTEXT=SINGLETON | ||
| # cd /home/renatob/ClimaLand.jl |
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.
can we remove this line since it's specific to your directory?
| # Create a test field with known values including some that exceed the cap | ||
| coords = ClimaCore.Fields.coordinate_field(surface_space) | ||
| test_heights = coords.lat .* 0 .+ FT(5.0) # Initialize to 5m |
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.
Same comment I left below -
This is a little simpler, but what you have is fine too
| # Create a test field with known values including some that exceed the cap | |
| coords = ClimaCore.Fields.coordinate_field(surface_space) | |
| test_heights = coords.lat .* 0 .+ FT(5.0) # Initialize to 5m | |
| # Create a spatially-varying height field | |
| field_height = zeros(surface_space) .+ FT(5) # Initialize to 5m |
| @@ -0,0 +1,248 @@ | |||
| using Test | |||
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.
Thank you for writing such good tests!! This file should be added to test/runtests.jl so it runs with the test suite
…ng, clarify docs, update tests, and clean up experiment scripts
| SurfaceFluxes = "0.12.3, 0.13" | ||
| Thermodynamics = "0.14.1, 0.15" | ||
| julia = "1.10" | ||
| JuliaFormatter = "2.1.7" |
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 don't recall why exactly, but we still use JuliaFormatter 1. (not 2.)
JuliaFormatter shouldn't be a dependency of ClimaLand, you can keep it in your default environment
do:
]add JuliaFormatter@1
this will install the latest v1
to reset this to main, do:
git checkout origin/main Project.toml
Largest changes are observed: Sensible Heat (ERA5): Bias ↓ (8.27 → 7.89 W m⁻²) ~5% improvement, RMSE same. Evaporative Fraction (ERA5): Bias slightly closer to 0 (−0.0626 → −0.0601) ~4% improvement, Phase same. (No RMSE.) Surface Upward LW (CERES): Bias closer to 0 (−5.85 → −5.52 W m⁻²) ~6% improvement, RMSE ~ +0.1, Phase ≈ same. All the other variables have minimal changes. |
Purpose
Add support for spatially-varying canopy height in ClimaLand vegetation model, replacing the fixed 1m default with realistic height data from CLM artifacts (capped at 8m).
Content
Core Implementation
clm_canopy_height()function to read spatially-varying canopy height from CLM artifact data (src/standalone/Vegetation/spatially_varying_parameters.jl)effective_canopy_height()function to cap heights at 8m for numerical stability while preserving spatial patternsPrescribedBiomassModelto support spatially-varying height through type parameterHTH(canopy height type)src/standalone/Vegetation/canopy_boundary_fluxes.jl)h_leafsynchronization withcanopy_heightinPlantHydraulicseffective_canopy_heightBoolean operationsTesting and Validation
Impact Assessment
Figure: Sensible heat flux comparison showing Run 1 (default 1m height), Run 2 (spatially-varying height), absolute difference, and percent change.
The spatially-varying canopy height primarily affects:
To-do