Skip to content

Commit

Permalink
Add @with_error, remove @generate_error_functions
Browse files Browse the repository at this point in the history
`@generate_error_functions` was not a great macro: it required adding
new names to a line every time a diagnostic is added, it was dealing
with several variables at the same time, and could not be used outside
of its scope of definition and/or called twice.

`@with_error` accomplishes a the same goals as
`@generate_error_functions` without the problems. The only difference is
that now `compute` functions have to be decorated.
  • Loading branch information
Sbozzolo committed Aug 14, 2024
1 parent d909007 commit 3cb1303
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 94 deletions.
26 changes: 22 additions & 4 deletions docs/src/diagnostics/users_diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,34 @@ Your diagnostics have now been written in netcdf files in your output folder.

When defining a custom diagnostic, follow these steps:
- Define how to compute your diagnostic variable from your model state and cache.
For example, let's say you want the bowen ratio (ratio between sensible heat and latent heat) in the Bucket model.
```
function compute_bowen_ratio!(out, Y, p, t, land_model::BucketModel)
For example, let's say you want the Bowen ratio (ratio between sensible heat and latent heat) in the Bucket model.
```
function compute_bowen_ratio!(out, Y, p, t, land_model::BucketModel)
if isnothing(out)
return copy(p.bucket.turbulent_fluxes.shf / p.bucket.turbulent_fluxes.lhf)
else
out .= p.bucket.turbulent_fluxes.shf / p.bucket.turbulent_fluxes.lhf
end
end
```
```
It is good practice to add error messages to inform other users that a diagnostic variable makes sense only with a
specific `land_model`. This can be accomplished by prepending the `@with_error` macro at the function declaration,
as in
```
import ClimaLand.Diagnostics: @witherror
@with_error function compute_bowen_ratio!(out, Y, p, t, land_model::BucketModel)
if isnothing(out)
return copy(p.bucket.turbulent_fluxes.shf / p.bucket.turbulent_fluxes.lhf)
else
out .= p.bucket.turbulent_fluxes.shf / p.bucket.turbulent_fluxes.lhf
end
end
```
So, when someone tries outputting the Bowen ratio with a different model (e.g., `SnowModel`), `ClimaLand` will produce the following message:
```
Cannot compute albedo with model = SnowModel
```
- Add that diagnostic variable to your list of variables
```
add_diagnostic_variable!(
Expand Down
80 changes: 67 additions & 13 deletions src/diagnostics/bucket_compute_methods.jl
Original file line number Diff line number Diff line change
@@ -1,30 +1,42 @@
# stored in p

function compute_albedo!(out, Y, p, t, land_model::BucketModel)
@with_error function compute_albedo!(out, Y, p, t, land_model::BucketModel)
if isnothing(out)
return copy(p.bucket.α_sfc)
else
out .= p.bucket.α_sfc
end
end

function compute_net_radiation!(out, Y, p, t, land_model::BucketModel)
@with_error function compute_net_radiation!(
out,
Y,
p,
t,
land_model::BucketModel,
)
if isnothing(out)
return copy(p.bucket.R_n)
else
out .= p.bucket.R_n
end
end

function compute_surface_temperature!(out, Y, p, t, land_model::BucketModel)
@with_error function compute_surface_temperature!(
out,
Y,
p,
t,
land_model::BucketModel,
)
if isnothing(out)
return copy(p.bucket.T_sfc)
else
out .= p.bucket.T_sfc
end
end

function compute_surface_specific_humidity!(
@with_error function compute_surface_specific_humidity!(
out,
Y,
p,
Expand All @@ -38,39 +50,63 @@ function compute_surface_specific_humidity!(
end
end

function compute_latent_heat_flux!(out, Y, p, t, land_model::BucketModel)
@with_error function compute_latent_heat_flux!(
out,
Y,
p,
t,
land_model::BucketModel,
)
if isnothing(out)
return copy(p.bucket.turbulent_fluxes.lhf)
else
out .= p.bucket.turbulent_fluxes.lhf
end
end

function compute_aerodynamic_resistance!(out, Y, p, t, land_model::BucketModel)
@with_error function compute_aerodynamic_resistance!(
out,
Y,
p,
t,
land_model::BucketModel,
)
if isnothing(out)
return copy(p.bucket.turbulent_fluxes.r_ae)
else
out .= p.bucket.turbulent_fluxes.r_ae
end
end

function compute_sensible_heat_flux!(out, Y, p, t, land_model::BucketModel)
@with_error function compute_sensible_heat_flux!(
out,
Y,
p,
t,
land_model::BucketModel,
)
if isnothing(out)
return copy(p.bucket.turbulent_fluxes.shf)
else
out .= p.bucket.turbulent_fluxes.shf
end
end

function compute_vapor_flux!(out, Y, p, t, land_model::BucketModel)
@with_error function compute_vapor_flux!(out, Y, p, t, land_model::BucketModel)
if isnothing(out)
return copy(p.bucket.turbulent_fluxes.vapor_flux)
else
out .= p.bucket.turbulent_fluxes.vapor_flux
end
end

function compute_surface_air_density!(out, Y, p, t, land_model::BucketModel)
@with_error function compute_surface_air_density!(
out,
Y,
p,
t,
land_model::BucketModel,
)
if isnothing(out)
return copy(p.bucket.ρ_sfc)
else
Expand All @@ -80,15 +116,21 @@ end

# stored in Y

function compute_soil_temperature!(out, Y, p, t, land_model::BucketModel)
@with_error function compute_soil_temperature!(
out,
Y,
p,
t,
land_model::BucketModel,
)
if isnothing(out)
return copy(Y.bucket.T)
else
out .= Y.bucket.T
end
end

function compute_subsurface_water_storage!(
@with_error function compute_subsurface_water_storage!(
out,
Y,
p,
Expand All @@ -102,15 +144,27 @@ function compute_subsurface_water_storage!(
end
end

function compute_surface_water_content!(out, Y, p, t, land_model::BucketModel)
@with_error function compute_surface_water_content!(
out,
Y,
p,
t,
land_model::BucketModel,
)
if isnothing(out)
return copy(Y.bucket.Ws)
else
out .= Y.bucket.Ws
end
end

function compute_snow_water_equivalent!(out, Y, p, t, land_model::BucketModel)
@with_error function compute_snow_water_equivalent!(
out,
Y,
p,
t,
land_model::BucketModel,
)
if isnothing(out)
return copy(Y.bucket.σS)
else
Expand Down
25 changes: 0 additions & 25 deletions src/diagnostics/define_diagnostics.jl
Original file line number Diff line number Diff line change
@@ -1,28 +1,3 @@
# General helper functions for undefined diagnostics for a particular model
error_diagnostic_variable(variable, land_model::T) where {T} =
error("Cannot compute $variable with model = $T")

# generate_error_functions is helper macro that generates the error message
# when the user tries calling something that is incompatible with the model
macro generate_error_functions(variable_names...)
functions = Expr[]
for variable in variable_names
function_name_sym = Symbol("compute_", variable, "!")
body = esc(quote
function $function_name_sym(_, _, _, _, land_model)
error_diagnostic_variable($variable, land_model)
end
end)
push!(functions, body)
end
return quote
$(functions...)
end
end

# TODO: Automatically generate this list from the names of the diagnostics
@generate_error_functions "soil_net_radiation" "soil_latent_heat_flux" "soil_aerodynamic_resistance" "soil_sensible_heat_flux" "vapor_flux" "soil_temperature" "soil_water_liquid" "infiltration" "soilco2_diffusivity" "soilco2_source_microbe" "stomatal_conductance" "medlyn_term" "canopy_transpiration" "rainfall" "snowfall" "pressure" "wind_speed" "specific_humidity" "air_co2" "radiation_shortwave_down" "radiation_longwave_down" "photosynthesis_net_leaf" "photosynthesis_net_canopy" "respiration_leaf" "vcmax25" "photosynthetically_active_radiation" "photosynthetically_active_radiation_absorbed" "photosynthetically_active_radiation_reflected" "photosynthetically_active_radiation_transmitted" "near_infrared_radiation" "near_infrared_radiation_absorbed" "near_infrared_radiation_reflected" "near_infrared_radiation_transmitted" "radiation_shortwave_net" "radiation_longwave_net" "autotrophic_respiration" "soilco2" "heterotrophic_respiration" "soil_hydraulic_conductivity" "soil_water_potential" "soil_thermal_conductivity" "solar_zenith_angle" "moisture_stress_factor" "canopy_water_potential" "cross_section" "cross_section_roots" "area_index" "canopy_latent_heat_flux" "canopy_sensible_heat_flux" "canopy_aerodynamic_resistance" "canopy_temperature" "soil_ice"

"""
define_diagnostics!(land_model)
Expand Down
38 changes: 38 additions & 0 deletions src/diagnostics/diagnostic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,44 @@ function get_diagnostic_variable(short_name)
return ALL_DIAGNOSTICS[short_name]
end

# General helper functions for undefined diagnostics for a particular model
error_diagnostic_variable(variable, land_model::T) where {T} =
error("Cannot compute $variable with model = $T")

# with_error is a helper macro that generates the error message
# when the user tries calling something that is incompatible with the model.
# It should be called when defining compute functions
macro with_error(compute_function_expr)
compute_function_expr.head == :function ||
error("Cannot parse this function, head is not a :function")

# Two firsts:
# 1st: extract the function signature
# 2nd: extract the name
function_name = first(first(compute_function_expr.args).args)
function_name isa Symbol || error("Cannot parse this function!")

# qualified_name ensures that this macro can be used outside of this module while
# still defining compute functions in this module
qualified_name = GlobalRef(Diagnostics, function_name)
method_already_exists = hasmethod(qualified_name, (Any, Any, Any, Any, Any))
if method_already_exists
return nothing
else
# Assuming the convention that functions are called "compute_variable!",
# otherwise the error might look a little less informative
variable_name =
replace(string(function_name), "compute_" => "", "!" => "")
return esc(
quote
function $qualified_name(_, _, _, _, land_model)
error_diagnostic_variable($variable_name, land_model)
end
end,
)
end
end

# Do you want to define more diagnostics? Add them here
include("bucket_compute_methods.jl")
include("soilcanopy_compute_methods.jl")
Expand Down
Loading

0 comments on commit 3cb1303

Please sign in to comment.