From b3839b5332bc3a9310a328a1b020648f30cd881b Mon Sep 17 00:00:00 2001 From: odunbar Date: Wed, 9 Feb 2022 13:50:43 -0800 Subject: [PATCH] Add new CLIMAParameters interface Enable get_parameter_values to deal with array-valued parameters - `get_parameter_values` extracts the requested parameter values and returns them as an array. The returned array contains the values corresponding to the given list of parameter names (unless a single scalar parameter is requested, in which case the function returns a float), regardless of whether these parameters are all of the same type. - E.g., given a list of parameter names ["scalar_param", "array_param"] with `scalar_param` having a scalar value and `array_param` being array-valued, the first element of the returned array will be a float, and the second element will be an array - Added more tests to `toml_consistency.jl` to check if parsing, extracting, and writing of array-valued parameters works. Also added a test for `merge_override_default_values` Enforce types and move `array_parameters.toml` to test folder Add V0 of TOML file parsing for UQ parameters Add functionality to write parameter ensembles to toml files Move docstrings above functions Add improvements and updates - save_parameter_ensemble now creates a separate subdirectory for each ensemble member, while the name of the saved parameter file after each ensemble Kalman update is the same for all members. Example: iteration_01/member_01/test_parameters.toml iteration_01/member_02/test_parameters.toml etc - correct saving of multidimensional parameters using parameter slices Move dict version of `write_log_file` from file_parsing.jl to file_parsing_uq.jl Within the CES API, `write_log_file` will take a parameter dictionary as input (rather than a parameter struct containing a parameter dictionary, as in the running-the-climate-model API). Since the two APIs will go separate ways in the future, the CES version of `write_log_file` should be located in `file_parsing_uq.jl`. added typo-checks for overrides, provides warnings or error on parameter logs slightly more revealing test documentation and guides Modify parsing of UQ parameters to work with new `ParameterDistribution` API Make tests work Add regularization flags Revise based on Ollie's comments, and remove print commands from tests moved toml and old files into directories parameter box example (as test set) Improve create_parameter_struct interface warn_else_error -> warn_or_error, and docs update Fix some docs, minor adjustments Apply formatter WIP Refactor --- Project.toml | 2 + docs/make.jl | 7 +- docs/src/API.md | 28 ++ docs/src/parameter_structs.md | 170 ++++++++++ docs/src/toml.md | 128 +++++++ src/CLIMAParameters.jl | 1 + src/file_parsing.jl | 371 ++++++++++++++++++++ src/parameters.toml | 581 ++++++++++++++++++++++++++++++++ test/param_boxes.jl | 59 ++++ test/runtests.jl | 2 + test/toml/array_parameters.toml | 14 + test/toml/override_typos.toml | 9 + test/toml/parambox.toml | 11 + test/toml_consistency.jl | 236 +++++++++++++ 14 files changed, 1618 insertions(+), 1 deletion(-) create mode 100644 docs/src/parameter_structs.md create mode 100644 docs/src/toml.md create mode 100644 src/file_parsing.jl create mode 100644 src/parameters.toml create mode 100644 test/param_boxes.jl create mode 100644 test/toml/array_parameters.toml create mode 100644 test/toml/override_typos.toml create mode 100644 test/toml/parambox.toml create mode 100644 test/toml_consistency.jl 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)