diff --git a/docs/src/tutorials.md b/docs/src/tutorials.md index e5f5135d..9d4a845f 100644 --- a/docs/src/tutorials.md +++ b/docs/src/tutorials.md @@ -98,6 +98,7 @@ input_dir = "../../test/inputs/Tiny" # hide # input_dir should be the path to Tiny as a string (something like "test/inputs/Tiny") connection = DBInterface.connect(DuckDB.DB) read_csv_folder(connection, input_dir; schemas = TulipaEnergyModel.schema_per_table_name) +model_parameters = ModelParameters(connection) graph, representative_periods, timeframe, groups, years = create_internal_structures(connection) ``` @@ -119,7 +120,7 @@ dataframes = construct_dataframes(graph, representative_periods, constraints_par Now we can compute the model. ```@example manual -model = create_model(graph, representative_periods, dataframes, years, timeframe, groups) +model = create_model(graph, representative_periods, dataframes, years, timeframe, groups, model_parameters) ``` Finally, we can compute the solution. diff --git a/src/TulipaEnergyModel.jl b/src/TulipaEnergyModel.jl index af8904eb..b4cb61c0 100644 --- a/src/TulipaEnergyModel.jl +++ b/src/TulipaEnergyModel.jl @@ -27,6 +27,7 @@ using TimerOutputs: TimerOutput, @timeit const to = TimerOutput() include("input-schemas.jl") +include("model-parameters.jl") include("structures.jl") include("io.jl") include("create-model.jl") diff --git a/src/create-model.jl b/src/create-model.jl index e27e34b0..74c695d6 100644 --- a/src/create-model.jl +++ b/src/create-model.jl @@ -537,6 +537,7 @@ function create_model!(energy_problem; kwargs...) constraints_partitions = energy_problem.constraints_partitions timeframe = energy_problem.timeframe groups = energy_problem.groups + model_parameters = energy_problem.model_parameters years = energy_problem.years energy_problem.dataframes = @timeit to "construct_dataframes" construct_dataframes( graph, @@ -550,7 +551,8 @@ function create_model!(energy_problem; kwargs...) energy_problem.dataframes, years, timeframe, - groups; + groups, + model_parameters; kwargs..., ) energy_problem.termination_status = JuMP.OPTIMIZE_NOT_CALLED @@ -574,7 +576,8 @@ function create_model( dataframes, years, timeframe, - groups; + groups, + model_parameters; write_lp_file = false, ) diff --git a/src/model-parameters.jl b/src/model-parameters.jl new file mode 100644 index 00000000..3a02e45b --- /dev/null +++ b/src/model-parameters.jl @@ -0,0 +1,61 @@ +export ModelParameters + +""" + ModelParameters(;key = value, ...) + ModelParameters(path; ...) + ModelParameters(connection; ...) + ModelParameters(connection, path; ...) + +Structure to hold the model parameters. +Some values are defined by default and some required explicit definition. + +If `path` is passed, it is expected to be a string pointing to a TOML file with +a `key = value` list of parameters. Explicit keyword arguments take precedence. + +If `connection` is passed, the default `discount_year` is set to the +minimum of all milestone years. In other words, we check for the table +`year_data` for the column `year` where the column `is_milestone` is true. +Explicit keyword arguments take precedence. + +If both are passed, then `path` has preference. Explicit keyword arguments take precedence. + +## Parameters + +- `discount_rate::Float64 = 0.0`: The model discount rate. +- `discount_year::Int`: The model discount year. +""" +Base.@kwdef mutable struct ModelParameters + discount_rate::Float64 = 0.0 + discount_year::Int # Explicit definition expected +end + +# Using `@kwdef` defines a default constructor based on keywords + +function _read_model_parameters(path) + if length(path) > 0 && !isfile(path) + throw(ArgumentError("path `$path` does not contain a file")) + end + + file_data = length(path) > 0 ? TOML.parsefile(path) : Dict{String,Any}() + file_parameters = Dict(Symbol(k) => v for (k, v) in file_data) + + return file_parameters +end + +function ModelParameters(path::String; kwargs...) + file_parameters = _read_model_parameters(path) + + return ModelParameters(; file_parameters..., kwargs...) +end + +function ModelParameters(connection::DuckDB.DB, path::String = ""; kwargs...) + discount_year = minimum( + row.year for + row in DuckDB.query(connection, "SELECT year FROM year_data WHERE is_milestone = true") + ) + # This can't be naively refactored to reuse the function above because of + # the order of preference of the parameters. + file_parameters = _read_model_parameters(path) + + return ModelParameters(; discount_year, file_parameters..., kwargs...) +end diff --git a/src/run-scenario.jl b/src/run-scenario.jl index a01c18d0..c38e59a7 100644 --- a/src/run-scenario.jl +++ b/src/run-scenario.jl @@ -16,12 +16,16 @@ function run_scenario( connection; output_folder = "", optimizer = HiGHS.Optimizer, + model_parameters_file = "", parameters = default_parameters(optimizer), write_lp_file = false, log_file = "", show_log = true, ) - energy_problem = @timeit to "create EnergyProblem from connection" EnergyProblem(connection) + energy_problem = @timeit to "create EnergyProblem from connection" EnergyProblem( + connection; + model_parameters_file, + ) @timeit to "create_model!" create_model!(energy_problem; write_lp_file) diff --git a/src/structures.jl b/src/structures.jl index d87b182b..2c5d5da5 100644 --- a/src/structures.jl +++ b/src/structures.jl @@ -290,6 +290,7 @@ It hides the complexity behind the energy problem, making the usage more friendl - `timeframe`: The number of periods of the `representative_periods`. - `dataframes`: The data frames used to linearize the variables and constraints. These are used internally in the model only. - `groups`: The input data of the groups to create constraints that are common to a set of assets in the model. +- `model_parameters`: The model parameters. - `model`: A JuMP.Model object representing the optimization model. - `solved`: A boolean indicating whether the `model` has been solved or not. - `objective_value`: The objective value of the solved problem. @@ -319,6 +320,7 @@ mutable struct EnergyProblem groups::Vector{Group} years::Vector{Year} dataframes::Dict{Symbol,DataFrame} + model_parameters::ModelParameters model::Union{JuMP.Model,Nothing} solution::Union{Solution,Nothing} solved::Bool @@ -327,12 +329,12 @@ mutable struct EnergyProblem timings::Dict{String,Float64} """ - EnergyProblem(connection) + EnergyProblem(connection; model_parameters_file = "") Constructs a new EnergyProblem object using the `connection`. This will call relevant functions to generate all input that is required for the model creation. """ - function EnergyProblem(connection) + function EnergyProblem(connection; model_parameters_file = "") elapsed_time_internal = @elapsed begin graph, representative_periods, timeframe, groups, years = create_internal_structures(connection) @@ -351,6 +353,7 @@ mutable struct EnergyProblem groups, years, Dict(), + ModelParameters(connection, model_parameters_file), nothing, nothing, false, diff --git a/test/Project.toml b/test/Project.toml index e95de825..762021c8 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -10,5 +10,6 @@ HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" MetaGraphsNext = "fa8bd995-216d-47f1-8a91-f3b68fbeb377" +TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TulipaIO = "7b3808b7-0819-42d4-885c-978ba173db11" diff --git a/test/inputs/model-parameters-example.toml b/test/inputs/model-parameters-example.toml new file mode 100644 index 00000000..6dd95372 --- /dev/null +++ b/test/inputs/model-parameters-example.toml @@ -0,0 +1,2 @@ +discount_rate = 0.03 +discount_year = 2020 diff --git a/test/runtests.jl b/test/runtests.jl index b36a4e9f..1429d14d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,6 +8,7 @@ using HiGHS using JuMP using MathOptInterface using Test +using TOML using TulipaEnergyModel using TulipaIO diff --git a/test/test-case-studies.jl b/test/test-case-studies.jl index 50307741..0f5f19b4 100644 --- a/test/test-case-studies.jl +++ b/test/test-case-studies.jl @@ -72,7 +72,10 @@ end dir = joinpath(INPUT_FOLDER, "Multi-year Investments") connection = DBInterface.connect(DuckDB.DB) _read_csv_folder(connection, dir) - energy_problem = run_scenario(connection) + energy_problem = run_scenario( + connection; + model_parameters_file = joinpath(@__DIR__, "inputs", "model-parameters-example.toml"), + ) # @test energy_problem.objective_value ≈ 28.45872 atol = 1e-5 end diff --git a/test/test-model-parameters.jl b/test/test-model-parameters.jl new file mode 100644 index 00000000..71ba6034 --- /dev/null +++ b/test/test-model-parameters.jl @@ -0,0 +1,50 @@ +@testset "Testing Model Parameters" begin + path = joinpath(@__DIR__, "inputs", "model-parameters-example.toml") + + @testset "Basic usage" begin + mp = ModelParameters(; discount_rate = 0.1, discount_year = 2018) + @test mp.discount_rate == 0.1 + @test mp.discount_year == 2018 + end + + @testset "Errors when missing required parameters" begin + @test_throws UndefKeywordError ModelParameters() + end + + @testset "Read from file" begin + mp = ModelParameters(path) + data = TOML.parsefile(path) + for (key, value) in data + @test value == getfield(mp, Symbol(key)) + end + + @testset "explicit keywords take precedence" begin + mp = ModelParameters(path; discount_year = 2019) + @test mp.discount_year == 2019 + end + + @testset "Errors if path does not exist" begin + @test_throws ArgumentError ModelParameters("nonexistent.toml") + end + end + + @testset "Read from DuckDB" begin + connection = DBInterface.connect(DuckDB.DB) + read_csv_folder(connection, joinpath(@__DIR__, "inputs", "Norse")) + mp = ModelParameters(connection) + @test mp.discount_year == 2030 + + @testset "path has precedence" begin + mp = ModelParameters(connection, path) + data = TOML.parsefile(path) + for (key, value) in data + @test value == getfield(mp, Symbol(key)) + end + end + + @testset "explicit keywords take precedence" begin + mp = ModelParameters(connection, path; discount_year = 2019) + @test mp.discount_year == 2019 + end + end +end