diff --git a/Project.toml b/Project.toml index d03bf7e9..61e1271d 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,8 @@ authors = ["Charles Kawczynski "] version = "0.4.3" [deps] +DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] diff --git a/docs/make.jl b/docs/make.jl index 6ffdb9c5..571d013c 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,6 +1,11 @@ using CLIMAParameters, Documenter -pages = Any["Home" => "index.md", "API" => "API.md"] +pages = Any[ + "Home" => "index.md", + "TOML file interface" => "toml.md", + "Parameter structures" => "parameter_structs.md", + "API" => "API.md", +] mathengine = MathJax( Dict( diff --git a/docs/src/API.md b/docs/src/API.md index 5b2e2c78..2d581288 100644 --- a/docs/src/API.md +++ b/docs/src/API.md @@ -4,6 +4,34 @@ CurrentModule = CLIMAParameters ``` +## Parameter struct + +```@docs +AbstractParamDict +ParamDict +AliasParamDict +``` + +## File parsing and parameter logging + +### User facing functions: +```@docs +create_parameter_struct +get_parameter_values! +get_parameter_values +float_type +log_parameter_information +``` + +### Internal functions: +```@docs +log_component! +get_values +check_override_parameter_usage +write_log_file +merge_override_default_values +``` + ## Types ```@docs diff --git a/docs/src/parameter_structs.md b/docs/src/parameter_structs.md new file mode 100644 index 00000000..bf8205f7 --- /dev/null +++ b/docs/src/parameter_structs.md @@ -0,0 +1,170 @@ +# Parameter Structures + +Parameters are stored in objects that reflect the model component construction. Definitions should be inserted into the model component source code + +## An example from `Thermodynamics.jl` + +### In the user-facing driver file +```julia +import CLIMAParameters +import Thermodynamics + +parameter_struct = CLIMAParameters.create_parameter_struct(;dict_type="alias") +thermo_params = Thermodynamics.ThermodynamicsParameters(parameter_struct) +``` + +### In the source code for `Thermodynamics.jl` + +```julia +Base.@kwdef struct ThermodynamicsParameters{FT} + gas_constant::FT + molmass_dryair::FT + ... + R_d::FT +end +``` +- The struct is parameterized by `{FT}` which is a user-determined float precision +- Only relevant parameters used in `Thermodynamics` are stored here. +- A keyword based `struct` so we do not rely on parameter order + +The constructor is as follows +```julia +function ThermodynamicsParameters(parameter_struct) + + # Used in thermodynamics, from parameter file + aliases = [ ..., "gas_constant", "molmass_dryair"] + + param_pairs = CLIMAParameters.get_parameter_values!( + param_struct, + aliases, + "Thermodynamics", + ) + nt = (; param_pairs...) + + # derived parameters from parameter file + R_d = nt.gas_constant / nt.molmass_dryair + + FT = CP.float_type(param_struct) + return ThermodynamicsParameters{FT}(; nt..., R_d) +end +``` + +- The constructor takes in a `parameter_struct` produced from reading the TOML file +- We list the aliases of parameters required by `Thermodynamics.jl` +- We obtain parameters (in the form of a list of (alias,value) Pairs) from `get_parameter_values!(parameter_struct,aliases,component_name)` The `component_name` is a string used for the parameter log. +- We convert to namedtuple for ease of extraction +- We create any `derived parameters` i.e. commonly used simple functions of parameters that are treated as parameters. here we create the dry air gas constant `R_d` +- We return the `ThermodynamicsParameters{FT}`, where FT is an enforced float type (e.g. single or double precision) + + +## An example with modular components from `CloudMicrophysics.jl` + +### In the user-facing driver file + +Here we build a `CloudMicrophysics` parameter set. In this case, the user wishes to use a +0-moment microphysics parameterization scheme. +```julia +import CLIMAParameters +import Thermodynamics +import CloudMicrophysics + +#load defaults +parameter_struct = CLIMAParameters.create_parameter_struct(; dict_type="alias") + +#build the low level parameter set +param_therm = Thermodynamics.ThermodynamicsParameters(parameter_struct) +param_0M = CloudMicrophysics.Microphysics_0M_Parameters(parameter_struct) + +#build the hierarchical parameter set +parameter_set = CloudMicrophysics.CloudMicrophysicsParameters( + parameter_struct, + param_0M, + param_therm +) +``` +!!! note + The exact APIs here are subject to change + +### In the source code for `CloudMicrophysics.jl` + +Build the different options for a Microphysics parameterizations +```julia +abstract type AbstractMicrophysicsParameters end +struct NoMicrophysicsParameters <: AbstractMicrophysicsParameters end +Base.@kwdef struct Microphysics_0M_Parameters{FT} <: AbstractMicrophysicsParameters + τ_precip::FT + qc_0::FT + S_0::FT +end +Base.@kwdef struct Microphysics_1M_Parameters{FT} <: AbstractMicrophysicsParameters + ... +end +``` +We omit their constructors (see above). The `CloudMicrophysics` parameter set is built likewise + +```julia +Base.@kwdef struct CloudMicrophysicsParameters{FT, AMPS <: AbstractMicrophysicsParameters} + K_therm::FT + ... + MPS::AMPS + TPS::ThermodynamicsParameters{FT} +end + + +function CloudMicrophysicsParameters( + parameter_struct, + MPS::AMPS, + TPS::ThermodynamicsParameters{FT}, +) where {FT, AMPS <: AbstractMicrophysicsParameters} + + aliases = [ "K_therm", ... ] + + param_pairs = CLIMAParameters.get_parameter_values!( + parameter_struct, + aliases, + "CloudMicrophysics", + ) + + nt = (; param_pairs...) + #derived parameters + ... + FT = CP.float_type(parameter_struct) + + return CloudMicrophysicsParameters{FT, AMPS}(; + nt..., + ... + MPS, + TPS, + ) +end +``` + +## Calling parameters from `src` + +!!! note + The exact APIs here are subject to change + +When building the model components, parameters are extracted by calling `param_set.name` or `param_set.alias` (currently) +```julia +function example_cloudmicrophysics_func(param_set::CloudMicrophysicsParameters,...) + K_therm = param_set.K_therm + ... +end +``` +When calling functions from dependent packages, simply pass the relevant lower_level parameter struct +```julia +function example_cloudmicrophysics_func(param_set::CloudMicrophysicsParameters,...) + thermo_output = Thermodynamics.thermo_function(param_set.TPS,...) + cm0_output = Microphysics_0m.microphys_function(param_set.MPS,...) + ... +end +``` +These functions should be written with this in mind (dispatching) +```julia +function microphys_function(param_set::Microphysics_0M_parameters,...) + qc_0 = param_set.qc_0 + ... +end +``` + + diff --git a/docs/src/toml.md b/docs/src/toml.md new file mode 100644 index 00000000..44e6c46b --- /dev/null +++ b/docs/src/toml.md @@ -0,0 +1,128 @@ +# The TOML parameter file interface + +The complete user interface consists of two files in `TOML` format +1. A user-defined experiment file - in the local experiment directory +2. A defaults file - in `src/` directory of `ClimaParameters.jl` + +## Parameter style-guide + +A parameter is determined by its unique name. It has possible attributes +1. `alias` +2. `value` +3. `type` +4. `description` +5. `prior` +6. `transformation` + +!!! warn + Currently we only support `float` and `array{float}` types. (option-type flags and string switches are not considered CLIMAParameters.) + +### Minimal parameter requirement to run in CliMA + +```TOML +[molar_mass_dry_air] +value = 0.03 +type = "float" +``` + +### A more informative parameter (e.g. found in the defaults file) + +```TOML +[molar_mass_dry_air] +alias = "molmass_dryair" +value = 0.02897 +type = "float" +description = "Molecular weight dry air (kg/mol)" +``` + +### A more complex parameter for calibration + +```TOML +[neural_net_entrainment] +alias = "c_gen" +value = [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0] +type = "array" +description = "NN weights to represent the non-dimensional entrainment function" +prior = "MvNormal(0,I)" +``` + +### Interaction of the files + +On read an experiment file, the default file is also read and any duplicate parameter attributes are overwritten +e.g. If the minimal example above was loaded from an experiment file, and the informative example above was in the defaults file, then the loaded parameter would look as follows: +``` TOML +[molar_mass_dry_air] +alias = "molmass_dryair" +value = 0.03 +type = "float" +description = "Molecular weight dry air (kg/mol)" +``` +Here, the `value` field has been overwritten by the experiment value + +## File and parameter interaction on with CliMA + +`ClimaParameters.jl` provides several methods to parse, merge, and log parameter information. + + +### Loading from file +We provide the following methods to load parameters from file +```julia +create_parameter_struct(Float64;override_filepath, default_filepath, dict_type="alias") +create_parameter_struct(Float64;override_filepath ; dict_type="alias") +create_parameter_struct(Float64; dict_type="name") +``` +- The `dict_type = "name"` or `"alias"` determines the method of lookup of parameters (by `name` or by `alias` attributes). +- The `Float64` (or `Float32`) defines the requested precision of the returned parameters. + +Typical usage involves passing the local parameter file +```julia +import CLIMAParameters +local_exp_file = joinpath(@__DIR__,"local_exp_parameters.toml") +parameter_struct = CLIMAParameters.create_parameter_struct(;local_exp_file) +``` +If no file is passed it will use only the defaults from `ClimaParameters.jl` (causing errors if required parameters are not within this list). + +!!! note + Currently we search by the `alias` field (`dict_type="alias"` by default), so all parameters need an `alias` field, if in doubt, set alias and name to match the current code name convention + +The parameter struct is then used to build the codebase (see relevant Docs page) + +### Logging parameters + +Once the CliMA components are built, it is important to log the parameters. We provide the following methodd +```julia +log_parameter_information(parameter_struct, filepath; strict=false) +``` + +Typical usage will be after building components and before running +```julia +import Thermodynamics +therm_params = Thermodynamics.ThermodynamicsParameters(parameter_struct) +#... build(thermodynamics model,therm_params) + +log_file = joinpath(@__DIR__,"parameter_log.toml") +CLIMAParameters.log_parameter_information(parameter_struct,log_file) + +# ... run(thermodynamics_model) +``` + +This function performs two tasks +1. It writes a parameter log file to `log_file`. +2. It performs parameter sanity checks. + +Continuing our previous example, imagine `molar_mass_dry_air` was extracted in `ThermodynamicsParameters`. Then the log file will contain: +``` TOML +[molar_mass_dry_air] +alias = "molmass_dryair" +value = 0.03 +type = "float" +description = "Molecular weight dry air (kg/mol)" +used_in = ["Thermodynamics"] +``` +The additional attribute `used_in` displays every CliMA component that used this parameter. + +!!! note + Log files are written in TOML format, and can be read back into the model + +!!! warn + It is assumed that all parameters in the local experiment file should be used, if not a warning is displayed when calling `log_parameter_information`. this is upgraded to an error exception by changing `strict` diff --git a/src/CLIMAParameters.jl b/src/CLIMAParameters.jl index 4c7e4c4c..e2f22212 100644 --- a/src/CLIMAParameters.jl +++ b/src/CLIMAParameters.jl @@ -3,6 +3,7 @@ module CLIMAParameters export AbstractParameterSet export AbstractEarthParameterSet +include("file_parsing.jl") include("types.jl") include("UniversalConstants.jl") diff --git a/src/file_parsing.jl b/src/file_parsing.jl new file mode 100644 index 00000000..79e6c51b --- /dev/null +++ b/src/file_parsing.jl @@ -0,0 +1,371 @@ +using TOML +using DocStringExtensions + + +export AbstractParamDict +export ParamDict, AliasParamDict + +export float_type, + get_parameter_values!, + get_parameter_values, + write_log_file, + log_parameter_information, + create_parameter_struct + +""" + AbstractParamDict{FT <: AbstractFloat} + +Abstract parameter dict. Two subtypes: + - [`ParamDict`](@ref) + - [`AliasParamDict`](@ref) +""" +abstract type AbstractParamDict{FT <: AbstractFloat} end + +const NAMESTYPE = + Union{AbstractVector{S}, NTuple{N, S} where {N}} where {S <: AbstractString} + +""" + ParamDict(data::Dict, override_dict::Union{Nothing,Dict}) + +Structure to hold information read-in from TOML +file, as well as a parametrization type `FT`. + +Uses the name to search + +# Fields + +$(DocStringExtensions.FIELDS) +""" +struct ParamDict{FT} <: AbstractParamDict{FT} + "dictionary representing a default/merged parameter TOML file" + data::Dict + "either a nothing, or a dictionary representing an override parameter TOML file" + override_dict::Union{Nothing, Dict} +end + +""" + AliasParamDict(data::Dict, override_dict::Union{Nothing,Dict}) + +Structure to hold information read-in from TOML +file, as well as a parametrization type `FT`. + +Uses the alias to search + +# Fields + +$(DocStringExtensions.FIELDS) +""" +struct AliasParamDict{FT} <: AbstractParamDict{FT} + "dictionary representing a default/merged parameter TOML file" + data::Dict + "either a nothing, or a dictionary representing an override parameter TOML file" + override_dict::Union{Nothing, Dict} +end + +""" + float_type(::AbstractParamDict) + +The float type from the parameter dict. +""" +float_type(::AbstractParamDict{FT}) where {FT} = FT + +function Base.iterate(pd::AliasParamDict) + it = iterate(pd.data) + if it !== nothing + return (Pair(it[1].second["alias"], it[1].second), it[2]) + else + return nothing + end +end + +function Base.iterate(pd::AliasParamDict, state) + it = iterate(pd.data, state) + if it !== nothing + return (Pair(it[1].second["alias"], it[1].second), it[2]) + else + return nothing + end +end + +Base.iterate(pd::ParamDict, state) = Base.iterate(pd.data, state) +Base.iterate(pd::ParamDict) = Base.iterate(pd.data) + + +""" + log_component!(pd::AbstractParamDict, names, component) + +Adds a new key,val pair: `("used_in",component)` to each +named parameter in `pd`. +Appends a new val: `component` if "used_in" key exists. +""" +function log_component!( + pd::AliasParamDict, + names::NAMESTYPE, + component::AbstractString, +) + component_key = "used_in" + data = pd.data + for name in names + for (key, val) in data + name ≠ val["alias"] && continue + data[key][component_key] = if component_key in keys(data[key]) + unique([data[key][component_key]..., component]) + else + [component] + end + end + end +end + +function log_component!( + pd::ParamDict, + names::NAMESTYPE, + component::AbstractString, +) + component_key = "used_in" + data = pd.data + for name in names + for (key, val) in data + name ≠ key && continue + data[key][component_key] = if component_key in keys(data[key]) + unique([data[key][component_key]..., component]) + else + [component] + end + end + end +end + +""" + get_values(pd::AbstractParamDict, names) + +gets the `value` of the named parameters. +""" +function get_values(pd::AliasParamDict, aliases::NAMESTYPE) + FT = float_type(pd) + data = pd.data + # TODO: use map + ret_values = [] + for alias in aliases + for (key, val) in data + alias ≠ val["alias"] && continue + param_value = val["value"] + elem = + eltype(param_value) != FT ? map(FT, param_value) : param_value + push!(ret_values, Pair(Symbol(alias), elem)) + end + end + return ret_values +end + +function get_values(pd::ParamDict, names::NAMESTYPE) + FT = float_type(pd) + data = pd.data + ret_values = map(names) do name + param_value = data[name]["value"] + elem = + eltype(param_value) != FT ? map(FT, param_value) : param_value + Pair(Symbol(name), elem) + end + return ret_values +end + +""" + get_parameter_values!( + pd::AbstractParamDict, + names::Union{String,Vector{String}}, + component::String + ) + +(Note the `!`) Gets the parameter values, and logs +the component (if given) where parameters are used. +""" +function get_parameter_values!( + pd::AbstractParamDict, + names::NAMESTYPE, + component::Union{AbstractString, Nothing} = nothing, +) + if !isnothing(component) + log_component!(pd, names, component) + end + return get_values(pd, names) +end + +get_parameter_values!( + pd::AbstractParamDict, + names::AbstractString, + args...; + kwargs..., +) = first(get_parameter_values!(pd, [names], args..., kwargs...)) + +""" + get_parameter_values(pd::AbstractParamDict, names) + +Gets the parameter values only. +""" +get_parameter_values( + pd::AbstractParamDict, + names::Union{NAMESTYPE, AbstractString}, +) = get_parameter_values!(pd, names, nothing) + +""" + check_override_parameter_usage(pd::ParamDict, strict) + +Checks if parameters in the ParamDict.override_dict have the +key "used_in" (i.e. were these parameters used within the model run). +Throws warnings in each where parameters are not used. Also throws +an error if `strict == true` . +""" +check_override_parameter_usage(pd::AbstractParamDict, strict::Bool) = + check_override_parameter_usage(pd, strict, pd.override_dict) + +check_override_parameter_usage(pd::AbstractParamDict, strict::Bool, ::Nothing) = + nothing + +function check_override_parameter_usage( + pd::AbstractParamDict, + strict::Bool, + override_dict, +) + unused_override = Dict() + for (key, val) in override_dict + logged_val = pd.data[key] + unused_override[key] = !("used_in" in keys(logged_val)) + end + if any(values(unused_override)) + unused_override_keys = collect(keys(unused_override)) + filter!(key -> unused_override[key], unused_override_keys) + @warn( + string( + "Keys are present in parameter file but not used", + "in the simulation. \n Typically this is due to", + "a mismatch in parameter name in toml and in source.", + "Offending keys: $(unused_override_keys)", + ) + ) + if strict + @error( + "At least one override parameter set and not used in simulation" + ) + error( + "Halting simulation due to unused parameters." * + "\n Typically this is due to a typo in the parameter name." * + "\n change `strict` flag to `true` to prevent this causing an exception", + ) + end + end + return nothing +end + +""" + write_log_file(pd::AbstractParamDict, filepath) + +Writes a log file of all used parameters of `pd` at +the `filepath`. This file can be used to rerun the experiment. +""" +function write_log_file(pd::AbstractParamDict, filepath::AbstractString) + used_parameters = Dict() + for (key, val) in pd.data + if "used_in" in keys(val) + used_parameters[key] = val + end + end + open(filepath, "w") do io + TOML.print(io, used_parameters) + end +end + + +""" + log_parameter_information( + pd::AbstractParamDict, + filepath; + strict::Bool = false + ) + +Writes the parameter log file at `filepath`; checks that +override parameters are all used. + +If `strict = true`, errors if override parameters are unused. +""" +function log_parameter_information( + pd::AbstractParamDict, + filepath::AbstractString; + strict::Bool = false, +) + #[1.] write the parameters to log file + write_log_file(pd, filepath) + #[2.] send warnings or errors if parameters were not used + check_override_parameter_usage(pd, strict) +end + + +""" + merge_override_default_values( + override_param_struct::AbstractParamDict{FT}, + default_param_struct::AbstractParamDict{FT} + ) where {FT} + +Combines the `default_param_struct` with the `override_param_struct`, +precedence is given to override information. +""" +function merge_override_default_values( + override_param_struct::PDT, + default_param_struct::PDT, +) where {FT, PDT <: AbstractParamDict{FT}} + data = default_param_struct.data + override_dict = override_param_struct.override_dict + for (key, val) in override_param_struct.data + if !(key in keys(data)) + data[key] = val + else + for (kkey, vval) in val # as val is a Dict too + data[key][kkey] = vval + end + end + end + return PDT(data, override_dict) +end + +""" + create_parameter_struct(FT; + override_file, + default_file, + dict_type="alias" + ) + +Creates a `ParamDict{FT}` struct, by reading and merging upto +two TOML files with override information taking precedence over +default information. +""" +function create_parameter_struct( + ::Type{FT}; + override_file::Union{Nothing, String} = nothing, + default_file::String = joinpath(@__DIR__, "parameters.toml"), + dict_type = "alias", +) where {FT <: AbstractFloat} + @assert dict_type in ("alias", "name") + PDT = _param_dict(dict_type, FT) + if isnothing(override_file) + return PDT(TOML.parsefile(default_file), nothing) + end + override_param_struct = + PDT(TOML.parsefile(override_file), TOML.parsefile(override_file)) + default_param_struct = PDT(TOML.parsefile(default_file), nothing) + + #overrides the defaults where they clash + return merge_override_default_values( + override_param_struct, + default_param_struct, + ) +end + +function _param_dict(s::String, ::Type{FT}) where {FT} + if s == "alias" + return AliasParamDict{FT} + elseif s == "name" + return ParamDict{FT} + else + error("Bad string option given") + end +end diff --git a/src/parameters.toml b/src/parameters.toml new file mode 100644 index 00000000..f9ed2d1c --- /dev/null +++ b/src/parameters.toml @@ -0,0 +1,581 @@ +[prandtl_number_0_businger] +alias = "Pr_0_Businger" +value = 0.74 +type = "float" + +[coefficient_a_m_businger] +alias = "a_m_Businger" +value = 4.7 +type = "float" + +[coefficient_a_h_businger] +alias = "a_h_Businger" +value = 4.7 +type = "float" + +[prandtl_number_0_gryanik] +alias = "Pr_0_Gryanik" +value = 0.98 +type = "float" + +[coefficient_a_m_gryanik] +alias = "a_m_Gryanik" +value = 5.0 +type = "float" + +[coefficient_a_h_gryanik] +alias = "a_h_Gryanik" +value = 5.0 +type = "float" + +[coefficient_b_m_gryanik] +alias = "b_m_Gryanik" +value = 0.3 +type = "float" + +[coefficient_b_h_gryanik] +alias = "b_h_Gryanik" +value = 0.4 +type = "float" + +[prandtl_number_0_grachev] +alias = "Pr_0_Grachev" +value = 0.98 +type = "float" + +[coefficient_a_m_grachev] +alias = "a_m_Grachev" +value = 5.0 +type = "float" + +[coefficient_a_h_grachev] +alias = "a_h_Grachev" +value = 5.0 +type = "float" + +[coefficient_b_m_grachev] +alias = "b_m_Grachev" +value = 0.7692307692307693 +type = "float" +description = "derived [coefficient_a_m_grachev] / 6.5" + +[coefficient_b_h_grachev] +alias = "b_h_Grachev" +value = 5.0 +type = "float" + +[coefficient_c_h_grachev] +alias = "c_h_Grachev" +value = 3.0 +type = "float" + +[von_karman_constant] +alias = "von_karman_const" +value = 0.4 +type = "float" + + + +# microphysics + + +[precipitation_timescale] +alias = "τ_precip" +value = 1000 +type = "float" + +[specific_humidity_precipitation_threshold] +alias = "qc_0" +value = 0.005 +type = "float" + +[supersaturation_precipitation_threshold] +alias = "S_0" +value = 0.02 +type = "float" + +[rain_drop_drag_coefficient] +alias = "C_drag" +value = 0.55 +type = "float" + +[thermal_conductivity_of_air ] +alias = "K_therm" +value = 0.024 +type = "float" + +[diffusivity_of_water_vapor] +alias = "D_vapor" +value = 0.0000226 +type = "float" + +[kinematic_viscosity_of_air] +alias = "ν_air" +value = 0.000016 +type = "float" + +[condensation_evaporation_timescale] +alias = "τ_cond_evap" +value = 10 +type = "float" + +[sublimation_deposition_timescale] +alias = "τ_sub_dep" +value = 10 +type = "float" + +[ice_snow_threshold_radius] +alias = "r_ice_snow" +value = 6.25e-5 +type = "float" + +[cloud_ice_size_distribution_coefficient_n0] +alias = "n0_ice" +value = 2e7 +type = "float" + +[cloud_ice_crystals_length_scale] +alias = "r0_ice" +value = 0.00001 +type = "float" + +[cloud_ice_mass_size_relation_coefficient_me] +alias = "me_ice" +value = 3 +type = "float" + +[cloud_ice_mass_size_relation_coefficient_chim] +alias = "χm_ice" +value = 1 +type = "float" + +[cloud_ice_mass_size_relation_coefficient_delm] +alias = "Δm_ice" +value = 0 +type = "float" + +[cloud_liquid_water_specific_humidity_autoconversion_threshold] +alias = "q_liq_threshold" +value = 0.0005 +type = "float" + +[rain_autoconversion_timescale] +alias = "τ_acnv_rai" +value = 1000 +type = "float" + +[rain_ventillation_coefficient_a] +alias = "a_vent_rai" +value = 1.5 +type = "float" + +[rain_ventillation_coefficient_b ] +alias = "b_vent_rai" +value = 0.53 +type = "float" + +[rain_drop_size_distribution_coefficient_n0] +alias = "n0_rai" +value = 1.6e7 +type = "float" + +[rain_drop_length_scale] +alias = "r0_rai" +value = 0.001 +type = "float" + +[rain_mass_size_relation_coefficient_me] +alias = "me_rai" +value = 3 +type = "float" + +[rain_cross_section_size_relation_coefficient_ae] +alias = "ae_rai" +value = 2 +type = "float" + +[rain_terminal_velocity_size_relation_coefficient_ve] +alias = "ve_rai" +value = 0.5 +type = "float" + +[rain_mass_size_relation_coefficient_chim] +alias = "χm_rai" +value = 1 +type = "float" + +[rain_mass_size_relation_coefficient_delm] +alias = "Δm_rai" +value = 0 +type = "float" + +[rain_cross_section_size_relation_coefficient_chia] +alias = "χa_rai" +value = 1 +type = "float" + +[rain_cross_section_size_relation_coefficient_dela] +alias = "Δa_rai" +value = 0 +type = "float" + +[rain_terminal_velocity_size_relation_coefficient_chiv] +alias = "χv_rai" +value = 1 +type = "float" + +[rain_terminal_velocity_size_relation_coefficient_delv] +alias = "Δv_rai" +value = 0 +type = "float" + +[cloud_ice_specific_humidity_autoconversion_threshold] +alias = "q_ice_threshold" +value = 0.000001 +type = "float" + +[snow_autoconversion_timescale] +alias = "τ_acnv_sno" +value = 100 +type = "float" + +[snow_ventillation_coefficient_a] +alias = "a_vent_sno" +value = 0.65 +type = "float" + +[snow_ventillation_coefficient_b] +alias = "b_vent_sno" +value = 0.44 +type = "float" + +[snow_flake_size_distribution_coefficient_mu] +alias = "μ_sno" +value = 4.36e9 +type = "float" + +[snow_flake_size_distribution_coefficient_nu] +alias = "ν_sno" +value = 0.63 +type = "float" + +[snow_flake_length_scale] +alias = "r0_sno" +value = 0.001 +type = "float" + +[snow_mass_size_relation_coefficient_me] +alias = "me_sno" +value = 2 +type = "float" + +[snow_cross_section_size_relation_coefficient] +alias = "ae_sno" +value = 2 +type = "float" + +[snow_terminal_velocity_size_relation_coefficient] +alias = "ve_sno" +value = 0.25 +type = "float" + +[snow_mass_size_relation_coefficient_chim] +alias = "χm_sno" +value = 1 +type = "float" + +[snow_mass_size_relation_coefficient_delm] +alias = "Δm_sno" +value = 0 +type = "float" + +[snow_cross_section_size_relation_coefficient_chia] +alias = "χa_sno" +value = 1 +type = "float" + +[snow_cross_section_size_relation_coefficient_dela] +alias = "Δa_sno" +value = 0 +type = "float" + +[snow_terminal_velocity_size_relation_coefficient_chiv] +alias = "χv_sno" +value = 1 +type = "float" + +[snow_terminal_velocity_size_relation_coefficient_delv] +alias = "Δv_sno" +value = 0 +type = "float" + +[cloud_liquid_rain_collision_efficiency] +alias = "E_liq_rai" +value = 0.8 +type = "float" + +[cloud_liquid_snow_collision_efficiency] +alias = "E_liq_sno" +value = 0.1 +type = "float" + +[cloud_ice_rain_collision_efficiency] +alias = "E_ice_rai" +value = 1 +type = "float" + +[cloud_ice_snow_collision_efficiency] +alias = "E_ice_sno" +value = 0.1 +type = "float" + +[rain_snow_collision_efficiency] +alias = "E_rai_sno" +value = 1 +type = "float" + + + +# Initial planet_parameters toml file + + + +[molar_mass_dry_air] +alias = "molmass_dryair" +value = 0.02897 +type = "float" + +[adiabatic_exponent_dry_air] +alias = "kappa_d" +value = 0.28571428571 +type = "float" +description = "(2/7)" + +[density_liquid_water] +alias = "ρ_cloud_liq" +value = 1000 +type = "float" + +[density_ice_water] +alias = "ρ_cloud_ice" +value = 916.7 +type = "float" + +[molar_mass_water] +alias = "molmass_water" +value = 0.01801528 +type = "float" + +[isobaric_specific_heat_vapor] +alias = "cp_v" +value = 1859 +type = "float" + +[isobaric_specific_heat_liquid] +alias = "cp_l" +value = 4181 +type = "float" + +[isobaric_specific_heat_ice] +alias = "cp_i" +value = 2100 +type = "float" + +[temperature_water_freeze] +alias = "T_freeze" +value = 273.15 +type = "float" + +[temperature_saturation_adjustment_min] +alias = "T_min" +value = 150 +type = "float" + +[temperature_saturation_adjustment_max] +alias = "T_max" +value = 1000 +type = "float" + +[temperature_homogenous_nucleation] +alias = "T_icenuc" +value = 233 +type = "float" + +[temperature_triple_point] +alias = "T_triple" +value = 273.16 +type = "float" + +[thermodynamics_temperature_reference] +alias = "T_0" +value = 273.16 +type = "float" + + +[latent_heat_vaporization_at_reference] +alias = "LH_v0" +value = 2500800 +type = "float" + +[latent_heat_sublimation_at_reference] +alias = "LH_s0" +value = 2834400 +type = "float" + +[pressure_triple_point] +alias = "press_triple" +value = 611.657 +type = "float" + +[surface_tension_water] +alias = "surface_tension_coeff" +value = 0.072 +type = "float" + +[entropy_dry_air] +alias = "entropy_dry_air" +value = 6864.8 +type = "float" + +[entropy_water_vapor] +alias = "entropy_water_vapor" +value = 10513.6 +type = "float" + +[entropy_reference_temperature] +alias = "entropy_reference_temperature" +value = 298.15 +type = "float" + +[density_ocean_reference] +alias = "ρ_ocean" +value = 1035 +type = "float" + +[specific_heat_ocean] +alias = "cp_ocean" +value = 3989.25 +type = "float" + +[planet_radius] +alias = "planet_radius" +value = 6371000 +type = "float" + +[day] +alias = "day" +value = 86400 +type = "float" + +[angular_velocity_planet_rotation] +alias = "Omega" +value = 0.000072921159 +type = "float" + +[gravitational_acceleration] +alias = "grav" +value = 9.81 +type = "float" + +[anomalistic_year_length] +alias = "year_anom" +value = 31558464 +type = "float" +description = "derived: 365.25 * [day]" + +[length_orbit_semi_major] +alias = "orbit_semimaj" +value = 149597870000 +type = "float" +description = "derived: 1 * [astronomical_unit]" + +[total_solar_irradiance] +alias = "tot_solar_irrad" +value = 1362 +type = "float" + +[epoch_time] +alias = "epoch" +value = 211813488000 +type = "float" +description = "derived: 2451545.0 * [day]" + +[mean_anomalistic_at_epoch] +alias = "mean_anom_epoch" +value = 6.24006014121 +type = "float" +description = "(357.52911 degrees) in radians" + +[orbit_obliquity_at_epoch] +alias = "obliq_epoch" +value = 0.408979125113246 +type = "float" +description = "(23.432777778 degrees) in radians" + +[longitude_perihelion_at_epoch] +alias = "lon_perihelion_epoch" +value = 4.938188299449 +type = "float" +description = "(282.937348 degrees) in radians" + +[orbit_eccentricity_at_epoch] +alias = "eccentricity_epoch" +value = 0.016708634 +type = "float" + +[longitude_perihelion] +alias = "lon_perihelion" +value = 4.938188299449 +type = "float" +description = "(282.937348 degrees) in radians" + +[mean_sea_level_pressure] +alias = "MSLP" +value = 101325 +type = "float" + +[temperature_mean_at_reference] +alias = "T_surf_ref" +value = 290 +type = "float" + +[temperature_min_at_reference] +alias = "T_min_ref" +value = 220 +type = "float" + +[gas_constant] +alias = "gas_constant" +value = 8.3144598 +type = "float" + +[light_speed] +alias = "light_speed" +value = 299792458 +type = "float" + +[planck_constant] +alias = "h_Planck" +value = 6.626e-34 +type = "float" + +[bolztmann_constant] +alias = "k_Boltzmann" +value = 1.381e-23 +type = "float" + +[stefan_boltzmann_constant] +alias = "Stefan" +value = 5.67e-8 +type = "float" + +[astronomical_unit] +alias = "astro_unit" +value = 149597870000 +type = "float" + +[avogadro_constant] +alias = "avogad" +value = 6.02214076e23 +type = "float" + diff --git a/test/param_boxes.jl b/test/param_boxes.jl new file mode 100644 index 00000000..026e8760 --- /dev/null +++ b/test/param_boxes.jl @@ -0,0 +1,59 @@ + +import CLIMAParameters +const CP = CLIMAParameters + +Base.@kwdef struct ParameterBox{FT} + molmass_dryair::FT + gas_constant::FT + R_d::FT + new_parameter::FT +end + +function ParameterBox(param_struct::CP.AbstractParamDict) + + aliases = ["molmass_dryair", "gas_constant", "new_parameter"] + + params = CP.get_parameter_values!(param_struct, aliases, "ParameterBox") + nt = (; params...) # NamedTuple + + #derived parameters + R_d = nt.gas_constant / nt.molmass_dryair + + FT = CP.float_type(param_struct) + return ParameterBox{FT}(; nt..., R_d) +end + + +@testset "Example use case: parameter box" begin + + # [1.] read from file + toml_file = joinpath(@__DIR__, "toml", "parambox.toml") + param_struct = CP.create_parameter_struct( + Float64; + override_file = toml_file, + dict_type = "alias", + ) + + # [2.] build + param_set = ParameterBox(param_struct) + + # [3.] log & checks(with warning) + mktempdir(@__DIR__) do path + logfilepath = joinpath(path, "logfilepath.toml") + @test_logs (:warn,) CP.log_parameter_information( + param_struct, + logfilepath, + ) + end + + # [4.] use + # from default + @test param_set.molmass_dryair ≈ 0.02897 + # overridden default + @test param_set.gas_constant ≈ 4.0 + # derived in constructor + @test param_set.R_d ≈ param_set.gas_constant / param_set.molmass_dryair + # from toml + @test param_set.new_parameter ≈ 19.99 + +end diff --git a/test/runtests.jl b/test/runtests.jl index 5944a6b4..793a7466 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,3 +1,5 @@ +include("toml_consistency.jl") +include("param_boxes.jl") include("old/planet.jl") include("old/subgrid_scale.jl") diff --git a/test/toml/array_parameters.toml b/test/toml/array_parameters.toml new file mode 100644 index 00000000..c00cb87c --- /dev/null +++ b/test/toml/array_parameters.toml @@ -0,0 +1,14 @@ +[array_parameter_1] +alias = "arr_1" +value = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] +type = "float" + +[array_parameter_2] +alias = "arr_2" +value = [0.0, 1.0, 1.0, 2.0, 3.0, 5.0, 8.0, 13.0, 21.0, 34.0] +type = "float" + +[gravitational_acceleration] +alias = "grav" +value = [9.81, 10.0] +type = "float" diff --git a/test/toml/override_typos.toml b/test/toml/override_typos.toml new file mode 100644 index 00000000..3c6e75b4 --- /dev/null +++ b/test/toml/override_typos.toml @@ -0,0 +1,9 @@ +[light_spede] +value = 10.0 +type = "float" +alias = "light_spede" + +[light_speed] +value = 10000.0 +type = "float" +alias = "light_speed" diff --git a/test/toml/parambox.toml b/test/toml/parambox.toml new file mode 100644 index 00000000..5297914d --- /dev/null +++ b/test/toml/parambox.toml @@ -0,0 +1,11 @@ +[gas_constant] +alias = "gas_constant" +value = 4.0 + +[param_2] +alias = "new_parameter" +value = 19.99 + +[param_3] +alias = "unusued_parameter" +value = 1.0 \ No newline at end of file diff --git a/test/toml_consistency.jl b/test/toml_consistency.jl new file mode 100644 index 00000000..c0eba8be --- /dev/null +++ b/test/toml_consistency.jl @@ -0,0 +1,236 @@ +using Test + +# import CLIMAParameters +import CLIMAParameters +const CP = CLIMAParameters + +# read parameters needed for tests +full_parameter_set = CP.create_parameter_struct(Float64; dict_type = "alias") + +const CPP = CP.Planet +struct EarthParameterSet <: CP.AbstractEarthParameterSet end +const param_set_cpp = EarthParameterSet() + +universal_constant_aliases = [ + "gas_constant", + "light_speed", + "h_Planck", + "k_Boltzmann", + "Stefan", + "astro_unit", + "avogad", +] + +# CP modules list: +module_names = [ + CP, + CP.Planet, + CP.SubgridScale, + # CP.Atmos.EDMF, + # CP.Atmos.SubgridScale, + CP.Atmos.Microphysics_0M, + CP.Atmos.Microphysics, + CP.SurfaceFluxes.UniversalFunctions, +] + +CP_parameters = Dict(mod => String.(names(mod)) for mod in module_names) +logfilepath1 = joinpath(@__DIR__, "toml", "log_file_test_1.toml") + + +@testset "parameter file interface tests" begin + + @testset "load with name or alias" begin + @test_throws AssertionError CP.create_parameter_struct( + Float64; + dict_type = "not name or alias", + ) + + end + + + @testset "TOML - CliMAParameters.jl consistency" begin + # tests to check parameter consistency of new toml files with existing + # CP defaults. + + + + k_found = [0] + for (k, v) in full_parameter_set #iterates over data (by alias) + + for mod in module_names + k_pair = CP.get_parameter_values(full_parameter_set, k) + k_value = last(k_pair) + if k in CP_parameters[mod] + k_found[1] = 1 + cp_k = getfield(mod, Symbol(k)) + if !(k in universal_constant_aliases) + @test (k_value ≈ cp_k(param_set_cpp)) + else #universal parameters have no argument + @test (k_value ≈ cp_k()) + end + #for the logfile test later: + CP.get_parameter_values!( + full_parameter_set, + k, + string(nameof(mod)), + ) + end + end + if k_found[1] == 0 + println("on trying alias: ", k) + @warn("did not find in any modules") + end + + k_found[1] = 0 + end + + #create a dummy log file listing where CLIMAParameter lives + CP.write_log_file(full_parameter_set, logfilepath1) + end + + @testset "Parameter logging" begin + + + #read in log file as new parameter file and rerun test. + full_parameter_set_from_log = CP.create_parameter_struct( + Float64; + override_file = logfilepath1, + dict_type = "alias", + ) + k_found = [0] + for (k, v) in full_parameter_set_from_log #iterates over data (by alias) + for mod in module_names + k_pair = CP.get_parameter_values(full_parameter_set_from_log, k) + k_value = last(k_pair) + if k in CP_parameters[mod] + k_found[1] = 1 + cp_k = getfield(mod, Symbol(k)) + if !(k in universal_constant_aliases) + @test (k_value ≈ cp_k(param_set_cpp)) + else #universal parameters have no argument + @test (k_value ≈ cp_k()) + end + end + end + if k_found[1] == 0 + println("on trying alias: ", k) + @warn("did not find in any modules") + end + k_found[1] = 0 + end + end + + @testset "parameter arrays" begin + # Tests to check if file parsing, extracting and logging of parameter + # values also works with array-valued parameters + + # Create parameter structs consisting of the parameters contained in the + # default parameter file ("parameters.toml") and additional (array valued) + # parameters ("array_parameters.toml"). + path_to_array_params = + joinpath(@__DIR__, "toml", "array_parameters.toml") + # parameter struct of type Float64 (default) + param_set = CP.create_parameter_struct( + Float64; + override_file = path_to_array_params, + dict_type = "name", + ) + # parameter struct of type Float32 + param_set_f32 = CP.create_parameter_struct( + Float32; + override_file = path_to_array_params, + dict_type = "name", + ) + + # true parameter values (used to check if values are correctly read from + # the toml file) + true_param_1 = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] + true_param_2 = [0.0, 1.0, 1.0, 2.0, 3.0, 5.0, 8.0, 13.0, 21.0, 34.0] + true_param_3 = [9.81, 10.0] + true_param_4 = 299792458 + true_params = [true_param_1, true_param_2, true_param_3, true_param_4] + param_names = [ + "array_parameter_1", + "array_parameter_2", + "gravitational_acceleration", + "light_speed", + ] + + # Let's assume that parameter_vector_1 and parameter_vector_2 are used + # in a module called "Test" + mod = "Test" + + # Get parameter values and add information on the module where the + # parameters are used. + for i in range(1, stop = length(true_params)) + + param_pair = + CP.get_parameter_values!(param_set, param_names[i], mod) + param = last(param_pair) + @test param == true_params[i] + # Check if the parameter is of the correct type. It should have + # the same type as the ParamDict, which is specified by the + # `float_type` argument to `create_parameter_struct`. + @test eltype(param) == Float64 + + param_f32_pair = + CP.get_parameter_values!(param_set_f32, param_names[i], mod) + param_f32 = last(param_f32_pair) + @test eltype(param_f32) == Float32 + + end + + # Get several parameter values (scalar and arrays) at once + params = CP.get_parameter_values(param_set, param_names) + for j in 1:length(param_names) + param_val = last(params[j]) + @test param_val == true_params[j] + end + + # Write parameters to log file + mktempdir(@__DIR__) do path + logfilepath2 = joinpath(path, "log_file_test_2.toml") + CP.write_log_file(param_set, logfilepath2) + end + + # `param_set` and `full_param_set` contain different values for the + # `gravitational_acceleration` parameter. The merged parameter set should + # contain the value from `param_set`. + full_param_set = CP.create_parameter_struct(Float64; dict_type = "name") + merged_param_set = + CP.merge_override_default_values(param_set, full_param_set) + grav_pair = CP.get_parameter_values( + merged_param_set, + "gravitational_acceleration", + ) + grav = last(grav_pair) + @test grav == true_param_3 + end + + @testset "checks for overrides" begin + full_param_set = CP.create_parameter_struct( + Float64; + override_file = joinpath(@__DIR__, "toml", "override_typos.toml"), + dict_type = "name", + ) + mod = "test_module_name" + CP.get_parameter_values!(full_param_set, "light_speed", mod) + + mktempdir(@__DIR__) do path + logfilepath3 = joinpath(path, "log_file_test_3.toml") + @test_logs (:warn,) CP.log_parameter_information( + full_param_set, + logfilepath3, + ) + @test_throws ErrorException CP.log_parameter_information( + full_param_set, + logfilepath3, + strict = true, + ) + end + end + + +end + +rm(logfilepath1; force = true)