Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use hardcoded filenames. #815

Merged
merged 9 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/ribasim_cli/src/ribasim_cli.jl
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function julia_main()::Cint
println("The model finished successfully")
0
else
t = Ribasim.datetime_since(model.integrator.t, model.config.starttime)
t = Ribasim.datetime_since(model.integrator.t, model.config.toml.starttime)
retcode = model.integrator.sol.retcode
println("The model exited at model time $t with return code $retcode")
println("See https://docs.sciml.ai/DiffEqDocs/stable/basics/solution/#retcodes")
Expand Down
2 changes: 1 addition & 1 deletion build/ribasim_cli/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
def test_ribasim_cli(model_constructor, tmp_path):
model = model_constructor()
assert isinstance(model, ribasim.Model)
model.write(tmp_path)
model.write(tmp_path / "ribasim.toml")

executable = (
Path(__file__).parents[2]
Expand Down
1 change: 1 addition & 0 deletions core/src/Ribasim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,6 @@ include("lib.jl")
include("io.jl")
include("create.jl")
include("bmi.jl")
include("consts.jl")

end # module Ribasim
10 changes: 5 additions & 5 deletions core/src/allocation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ function add_variables_absolute_value!(
allocgraph_edge_ids_user_demand::Dict{Int, Int},
config::Config,
)::Nothing
if startswith(config.allocation.objective_type, "linear")
if startswith(config.toml.allocation.objective_type, "linear")
problem[:F_abs] =
JuMP.@variable(problem, F_abs[values(allocgraph_edge_ids_user_demand)])
end
Expand Down Expand Up @@ -545,14 +545,14 @@ function add_constraints_absolute_value!(
allocgraph_edge_ids_user_demand::Dict{Int, Int},
config::Config,
)::Nothing
objective_type = config.allocation.objective_type
objective_type = config.toml.allocation.objective_type
if startswith(objective_type, "linear")
allocgraph_edge_ids_user_demand = collect(values(allocgraph_edge_ids_user_demand))
F = problem[:F]
F_abs = problem[:F_abs]
d = 2.0

if config.allocation.objective_type == "linear_absolute"
if config.toml.allocation.objective_type == "linear_absolute"
# These constraints together make sure that F_abs acts as the absolute
# value F_abs = |x| where x = F-d (here for example d = 2)
problem[:abs_positive] = JuMP.@constraint(
Expand All @@ -567,7 +567,7 @@ function add_constraints_absolute_value!(
F_abs[i] >= -(F[i] - d),
base_name = "abs_negative"
)
elseif config.allocation.objective_type == "linear_relative"
elseif config.toml.allocation.objective_type == "linear_relative"
# These constraints together make sure that F_abs acts as the absolute
# value F_abs = |x| where x = 1-F/d (here for example d = 2)
problem[:abs_positive] = JuMP.@constraint(
Expand Down Expand Up @@ -689,7 +689,7 @@ function AllocationModel(
)

return AllocationModel(
Symbol(config.allocation.objective_type),
Symbol(config.toml.allocation.objective_type),
allocation_network_id,
subnetwork_node_ids,
node_id_mapping,
Expand Down
57 changes: 29 additions & 28 deletions core/src/bmi.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ end
Initialize a [`Model`](@ref) from a [`Config`](@ref).
"""
function BMI.initialize(T::Type{Model}, config::Config)::Model
alg = algorithm(config.solver)
db_path = input_path(config, config.database)
alg = algorithm(config.toml.solver)
db_path = input_path(config, config.toml.database)
if !isfile(db_path)
throw(SystemError("Database file not found: $db_path"))
end

# Setup timing logging
if config.logging.timing
if config.toml.logging.timing
TimerOutputs.enable_debug_timings(Ribasim) # causes recompilation (!)
end

Expand Down Expand Up @@ -63,9 +63,9 @@ function BMI.initialize(T::Type{Model}, config::Config)::Model
# tell the solver to stop when new data comes in
# TODO add all time tables here
time_flow_boundary = load_structvector(db, config, FlowBoundaryTimeV1)
tstops_flow_boundary = get_tstops(time_flow_boundary.time, config.starttime)
tstops_flow_boundary = get_tstops(time_flow_boundary.time, config.toml.starttime)
time_user = load_structvector(db, config, UserTimeV1)
tstops_user = get_tstops(time_user.time, config.starttime)
tstops_user = get_tstops(time_user.time, config.toml.starttime)
tstops = sort(unique(vcat(tstops_flow_boundary, tstops_user)))

# use state
Expand All @@ -91,21 +91,21 @@ function BMI.initialize(T::Type{Model}, config::Config)::Model
# Integrals for PID control
integral = zeros(length(parameters.pid_control.node_id))
u0 = ComponentVector{Float64}(; storage, integral)
t_end = seconds_since(config.endtime, config.starttime)
t_end = seconds_since(config.toml.endtime, config.toml.starttime)
# for Float32 this method allows max ~1000 year simulations without accuracy issues
@assert eps(t_end) < 3600 "Simulation time too long"
t0 = zero(t_end)
timespan = (t0, t_end)

jac_prototype = config.solver.sparse ? get_jac_prototype(parameters) : nothing
jac_prototype = config.toml.solver.sparse ? get_jac_prototype(parameters) : nothing
RHS = ODEFunction(water_balance!; jac_prototype)

@timeit_debug to "Setup ODEProblem" begin
prob = ODEProblem(RHS, u0, timespan, parameters)
end
@debug "Setup ODEProblem."

callback, saved_flow = create_callbacks(parameters, config; config.solver.saveat)
callback, saved_flow = create_callbacks(parameters, config; config.toml.solver.saveat)
@debug "Created callbacks."

# Initialize the integrator, providing all solver options as described in
Expand All @@ -121,19 +121,19 @@ function BMI.initialize(T::Type{Model}, config::Config)::Model
callback,
tstops,
isoutofdomain = (u, p, t) -> any(<(0), u.storage),
config.solver.saveat,
config.solver.adaptive,
dt = something(config.solver.dt, t0),
config.solver.dtmin,
dtmax = something(config.solver.dtmax, t_end),
config.solver.force_dtmin,
config.solver.abstol,
config.solver.reltol,
config.solver.maxiters,
config.toml.solver.saveat,
config.toml.solver.adaptive,
dt = something(config.toml.solver.dt, t0),
config.toml.solver.dtmin,
dtmax = something(config.toml.solver.dtmax, t_end),
config.toml.solver.force_dtmin,
config.toml.solver.abstol,
config.toml.solver.reltol,
config.toml.solver.maxiters,
)
@debug "Setup integrator."

if config.logging.timing
if config.toml.logging.timing
@show Ribasim.to
end

Expand All @@ -149,27 +149,27 @@ Write all results to the configured files.
"""
function BMI.finalize(model::Model)::Model
(; config) = model
(; results) = model.config
(; results) = model.config.toml
compress = get_compressor(results)

# basin
table = basin_table(model)
path = results_path(config, results.basin)
path = results_path(config, BASIN_NAME)
write_arrow(path, table, compress)

# flow
table = flow_table(model)
path = results_path(config, results.flow)
path = results_path(config, FLOW_NAME)
write_arrow(path, table, compress)

# discrete control
table = discrete_control_table(model)
path = results_path(config, results.control)
path = results_path(config, CONTROL_NAME)
write_arrow(path, table, compress)

# allocation
table = allocation_table(model)
path = results_path(config, results.allocation)
path = results_path(config, ALLOCATION_NAME)
write_arrow(path, table, compress)

@debug "Wrote results."
Expand Down Expand Up @@ -218,10 +218,10 @@ function create_callbacks(
tabulated_rating_curve_cb = PresetTimeCallback(tstops, update_tabulated_rating_curve!)
push!(callbacks, tabulated_rating_curve_cb)

if config.allocation.use_allocation
if config.toml.allocation.use_allocation
allocation_cb = PeriodicCallback(
update_allocation!,
config.allocation.timestep;
config.toml.allocation.timestep;
initial_affect = false,
)
push!(callbacks, allocation_cb)
Expand Down Expand Up @@ -568,7 +568,8 @@ end

BMI.get_current_time(model::Model) = model.integrator.t
BMI.get_start_time(model::Model) = 0.0
BMI.get_end_time(model::Model) = seconds_since(model.config.endtime, model.config.starttime)
BMI.get_end_time(model::Model) =
seconds_since(model.config.toml.endtime, model.config.toml.starttime)
BMI.get_time_units(model::Model) = "s"
BMI.get_time_step(model::Model) = get_proposed_dt(model.integrator)

Expand All @@ -592,10 +593,10 @@ function run(config::Config)::Model

# Reconfigure the logger if necessary with the correct loglevel
# but make sure to only log from Ribasim
if min_enabled_level(logger) + 1 != config.logging.verbosity
if min_enabled_level(logger) + 1 != config.toml.logging.verbosity
logger = EarlyFilteredLogger(
is_current_module,
LevelOverrideLogger(config.logging.verbosity, logger),
LevelOverrideLogger(config.toml.logging.verbosity, logger),
)
end

Expand Down
26 changes: 14 additions & 12 deletions core/src/config.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ using Logging: LogLevel, Debug, Info, Warn, Error
using ..Ribasim: Ribasim, isnode, nodetype
using OrdinaryDiffEq

export Config, Solver, Results, Logging
export Config, Solver, Results, Logging, Toml
export algorithm, snake_case, zstd, lz4

const schemas =
Expand Down Expand Up @@ -109,10 +109,6 @@ end

# Separate struct, as basin clashes with nodetype
@option struct Results <: TableOption
basin::String = "results/basin.arrow"
flow::String = "results/flow.arrow"
control::String = "results/control.arrow"
allocation::String = "results/allocation.arrow"
outstate::Union{String, Nothing} = nothing
compression::Compression = "zstd"
compression_level::Int = 6
Expand All @@ -129,17 +125,16 @@ end
objective_type::String = "quadratic_relative"
end

@option @addnodetypes struct Config <: TableOption
@option @addnodetypes struct Toml <: TableOption
starttime::DateTime
endtime::DateTime

# optional, when Config is created from a TOML file, this is its directory
relative_dir::String = "." # ignored(!)
input_dir::String = "."
results_dir::String = "."
# when Config is created from a TOML file, this is its directory
input_dir::String
Hofer-Julian marked this conversation as resolved.
Show resolved Hide resolved
results_dir::String

# input, required
database::String
# input
database::String = "database.gpkg"

allocation::Allocation = Allocation()
solver::Solver = Solver()
Expand All @@ -149,6 +144,13 @@ end
results::Results = Results()
end

struct Config
toml::Toml
relative_dir::String
end
Copy link
Member

@visr visr Nov 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like separating the TOML from other things. I don't like having to always type e.g. config.toml.starttime. Would it work for you if we forward Base.getproperty on Config to the toml field? To maintain the old API. To access the relative_dir field, which perhaps should just be dir or directory, we could add Base.dirname(config::Config) = getfield(config, :relative_dir).

The Base.show method in this file probably also need updating.

Copy link
Contributor

@Hofer-Julian Hofer-Julian Nov 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forward Base.getproperty on Config to the toml field

Nice idea. It adds a bit complexity, but I think it's worth it.

relative_dir field, which perhaps should just be dir or directory

I think relative_dir is more descriptive

we could add Base.dirname(config::Config) = getfield(config, :relative_dir)

The docs of Base.dirname talk about paths.
Since config isn't a path, I don't think it is applicable here. I've created a function relative_dir instead.

The Base.show method in this file probably also need updating.

It seems to work fine for me. It's what is called if I evaluate a Config instance in the REPL, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Not sure what you think, but the name relative_dir has always been a bit confusing to me. It is not necessarily relative, it is just a directory, it's other paths in the TOML that are relative to it.

Regarding Base.dirname, good that you checked the docstring for it. The line "Get the directory part of a path." really seems to describe the particular ::String method though. If it is a true function docstring it should describe what is does ("get the directory"), not from where. And what it does seems to match fine. Wflow also has this method on their Config.

Indeed Base.show seems to work fine, I mistakenly assumed it would be broken by this PR.


Config(toml::Toml) = Config(toml, ".")

function Configurations.from_dict(::Type{Logging}, ::Type{LogLevel}, level::AbstractString)
level == "debug" && return Debug
level == "info" && return Info
Expand Down
4 changes: 4 additions & 0 deletions core/src/consts.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
BASIN_NAME = "basin.arrow"
FLOW_NAME = "flow.arrow"
CONTROL_NAME = "control.arrow"
ALLOCATION_NAME = "allocation.arrow"
visr marked this conversation as resolved.
Show resolved Hide resolved
Loading