CliMA / ClimaAtmos.jl

ClimaAtmos.jl is a library for building atmospheric circulation models that is designed from the outset to leverage data assimilation and machine learning tools. We welcome contributions!
Apache License 2.0
72 stars 14 forks source link

Clean Up The Driver (🧹🚗🧹) #427

Open dennisYatunin opened 2 years ago

dennisYatunin commented 2 years ago

Purpose

Thanks to recent work by @charleskawczynski, the driver is now unified, in that it is localized to driver.jl plus a small number of included files, and every simulation can be run with julia --project driver.jl <command-line args>. Although we can start adding features like EDMF and coupling now, there are several issues with the current setup that have been becoming increasingly problematic, and we should probably sort them out before adding major new features. These issues are:

This proposal describes a series of PRs which will fix these issues and hopefully simplify our upcoming integration efforts.

Note that this proposal is not The Great Modeling Interface Redesign; although it changes some of our top-level data structures, it does not introduce any major new functionality like automatic construction of the Jacobian and application of boundary conditions. That will come later.

Cost/benefits/risks

The downside of this proposal is that the driver will continue to get modified for another 1--2 weeks. However, the resulting interface will be better suited for interactive debugging, as the code will be easier to trace through and call from the REPL. Also, it will be easier to move our code into /src and turn it into a package that can be called from other codebases.

Producers

@dennisYatunin will implement this proposal.

Components

Each of these new components will reside in its own module, so that we can avoid cluttering the global namespace. Here is a sketch of how some of the data structures will look:

Base.@kwdef mutable struct DriverConfig
    # Simulation parameters
    model::AbstractModel = StaggeredNonhydrostaticModel()
    initial_condition::AbstractInitialCondition
    postprocessing::Function # use custom type?

    # Domain parameters
    horizontal_mesh::Meshes.AbstractMesh
    quad::Spaces.Quadratures.QuadratureStyle
    z_max::Real
    z_elem::Int
    z_stretch::Meshes.StretchingRule = Meshes.Uniform()
    t_end::Real

    # Solver parameters
    ode_algorithm::OrdinaryDiffEqAlgorithm
    dt::Real
    dt_save_to_sol::Real = 0
    dt_save_to_disk::Real = 0
    max_newton_iters::Int = 10
    additional_callbacks::NTuple{N, DECallback} where {N} = ()
    additional_solver_kwargs::NamedTuple = (;)
    show_progress_bar::Bool = isinteractive()
end

struct RRTMGPRadiation{
    M <: AbstractRadiationMode,
    I <: AbstractInterpolation,
    B <: AbstractBottomExtrapolation,
    FT,
} <: AbstractTendency
    radiation_mode::M
    interpolation::I
    bottom_extrapolation::B
    idealized_insolation::Bool
    idealized_h2o::Bool
    update_frequency::FT
end

Base.@kwdef struct StaggeredNonhydrostaticModel{
    T <: Type{<:AbstractFloat},
    P <: CLIMAParameters.AbstractEarthParameterSet,
    U <: Union{Nothing, Operators.AdvectionOperator},
    E <: NTuple{N, AbstractTendency} where {N},
    J <: NamedTuple,
} <: AbstractModel
    FT::T = Float32
    params::P = DefaultEarthParameterSet()
    explicit_tendencies::E = (DefaultExplicitTendency(),)
    ᶠupwind_product::U = nothing
    jacobian_flags::J = (;)
    test_implicit_solver::Bool = false
end

struct DycoreTestingInitialCondition <: AbstractInitialCondition
    energy_var::Symbol
    moisture_mode::Symbol
    is_balanced_flow::Bool
end

function dycore_testing_post_processing(sol, output_dir)
    # make animations and plots
end

Inputs

N/A

Results and deliverables

Since this is an interface redesign, the main performance metric will be whether people find that it is easier to run simulations.

Task breakdown

Reviewers

@charleskawczynski @szy21 @jiahe23 @bischtob

szy21 commented 2 years ago

Looks good to me!

bischtob commented 2 years ago

I'm in favor.

charleskawczynski commented 2 years ago

This sounds great. I'd like to propose that we make these changes, incrementally since there's never really a great time for massive changes.

Regarding point 1, we could easily do something like this:

include("examples/cli_options.jl")
(s, parsed_args) = parse_commandline()
# edit parsed_args in the REPL
include("examples/driver.jl")

where, in the driver, we have

include("cli_options.jl")

if !(@isdefined parsed_args)
    (s, parsed_args) = parse_commandline()
end

I'm completely on board with the other two points-- the include structure needs to be adjusted, and the model composition helper functions should be made more consistent.

dennisYatunin commented 2 years ago

The reason we probably want something more advanced is that different configurations will require different command line arguments. That is, --dycore_testing_config and --column_equilibrium_config are commands (rather than flags), which change how arguments are parsed afterward. So, yes, DriverConfig is essentially doing the job currently fulfilled by parsed_args, but generalizing things in this way will allow us to be more flexible in terms of which parameters can be modified for different configurations.

dennisYatunin commented 2 years ago

My current vision for what will be at the top of driver.jl is

if isinteractive()
    @isdefined(truncate_stack_traces) || truncate_stack_traces = true
    @isdefined(driver_config) || error("`driver_config` must be defined before `include(\"driver.jl\")`")
else
    parsed_args = ArgParse.parse_args(ARGS, arg_parse_settings; as_symbols = true)
    truncate_stack_traces = parsed_args[:truncate_stack_traces]
    config_symbol = parsed_args[:_COMMAND_]
    if config_symbol == :dycore_testing_config
        driver_config = dycore_testing_config(; parsed_args[:dycore_testing_config]...)
    elseif config_symbol == :column_equilibrium_config
        driver_config = column_equilibrium_config(; parsed_args[:column_equilibrium_config]...)
    else
        error("unknown driver configuration $config_symbol")
    end
end
dennisYatunin commented 2 years ago

Another reason we want something more advanced than parsed_args to store a driver configuration is that parsed_args can only contain simple objects like numbers or strings that can be entered through the command line, while a DriverConfig can contain anything. If I want to add a new tendency with a DriverConfig, I just define that tendency's struct and add an instance of it to driver_config.model.explicit_tendencies. I don't see any analogous way to do this using parsed_args. Of course, I could modify how parsed_args is generated and interpreted by modifying our source code, but the goal of this proposal is to allow us to develop in the REPL without needing to modify source code for every minor change.