diff --git a/.gitignore b/.gitignore index a2894a0..de2065a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /test/Manifest.toml docs/build docs/Manifest.toml +docs/src/tutorials/*.md \ No newline at end of file diff --git a/docs/src/manual/discount.md b/docs/src/manual/discount.md index 640edc3..f24ec7d 100644 --- a/docs/src/manual/discount.md +++ b/docs/src/manual/discount.md @@ -24,6 +24,18 @@ While it is often normal to assume investments at the start of each strategic period, it can be more correct to average the discount factor for operational costs that are accrued throughout the strategic period. +We also provide a method in which the average discount factor is calculated for the beginning of the years within a strategic period: + +```@repl ts +using TimeStruct +ts = TwoLevel(5, 10, SimpleTimes(5,1)); +sps = strategic_periods(ts) +df_start = [discount(sp, ts, 0.05; type = "avg") for sp in sps] +df_avg = [discount(sp, ts, 0.05; type = "avg_year") for sp in sps] +``` + +This approach results in a slighly higher discount factor. + To help setting up the objective function in a typical optimization problem, there is a utility function [`objective_weight`](@ref) that returns the weight to give a time period in the objective, considering both diff --git a/docs/src/reference/api.md b/docs/src/reference/api.md index b70ca86..3abcc5b 100644 --- a/docs/src/reference/api.md +++ b/docs/src/reference/api.md @@ -1,154 +1,89 @@ -# [Available time structures](@id api-ts) +# [Interface for using `TimeStruct](@id api) -```@docs -TimeStructure -``` +## [Available time structures](@id api-ts) -```@docs -SimpleTimes -``` +### [Abstract supertypes](@id api-ts-abstract) ```@docs -CalendarTimes +TimeStructure ``` -```@docs -RepresentativePeriods -``` +### [Conecrete types](@id api-ts-con) ```@docs +SimpleTimes +CalendarTimes OperationalScenarios -``` - -```@docs +RepresentativePeriods TwoLevel -``` - -```@docs TwoLevelTree regular_tree ``` -# [Properties of time structures](@id api-prop_ts) +## [Properties of time structures](@id api-prop_ts) ```@docs mult_scen -``` - -```@docs mult_repr -``` - -```@docs mult_strat -``` - -```@docs probability_scen ``` -# [Iterating time structures](@id api-iter) +## [Iterating time structures](@id api-iter) -```@docs -repr_periods -``` +### [For individual time structures](@id api-iter-ts) ```@docs +repr_periods opscenarios -``` - -```@docs strat_periods strategic_periods -``` - -```@docs strategic_scenarios ``` -```@docs -withprev -``` +### [Specialized iterators](@id api-iter-spec) ```@docs +withprev withnext -``` - -```@docs chunk -``` - -```@docs chunk_duration ``` -# [Properties of time periods](@id api-prop_per) +## [Properties of time periods](@id api-prop_per) ```@docs duration -``` - -```@docs isfirst -``` - -```@docs multiple -``` - -```@docs multiple_strat -``` - -```@docs probability -``` - -```@docs start_oper_time -``` - -```@docs end_oper_time ``` - -# [Time profiles](@id api-prof) +## [Time profiles](@id api-prof) ```@docs FixedProfile -``` - -```@docs OperationalProfile -``` - -```@docs RepresentativeProfile -``` - -```@docs ScenarioProfile -``` - -```@docs StrategicProfile -``` - -```@docs StrategicStochasticProfile ``` -# [Discounting](@id api-disc) +## [Discounting](@id api-disc) -```@docs -discount -``` +### [Type](@id api-disc-type) ```@docs -objective_weight +Discounter ``` +### [Functions](@id api-disc-type) + ```@docs -Discounter +discount +objective_weight ``` diff --git a/src/discount.jl b/src/discount.jl index c2e20ac..5e6e7b6 100644 --- a/src/discount.jl +++ b/src/discount.jl @@ -1,22 +1,34 @@ +DiscPeriods = Union{TimePeriod,AbstractOperationalScenario,AbstractRepresentativePeriod} + """ - Discounter(discount_rate, timeunit_to_year, ts) + Discounter(discount_rate, ts::TimeStructure) + Discounter(discount_rate, timeunit_to_year, ts::TimeStructure) + +Structure to hold discount information to be used for a time structure `ts`. The +`discount_rate` is an absolute discount rate while the parameter `timeunit_to_year` is used +convert the time units of strategic periods in the time structure to years (default value = 1.0). + +As an example, consider the following time structure: -Structure to hold discount information to be used for a time structure. +```julia +# Modelling of a day with hourly resolution for 50 years with a resolution of 5 years +periods = TwoLevel(10, 5 * 8760, SimpleTimes(24, 1)) + +# The parameter `timeunit_to_year` must in this case be 1 year / 8760 h +disc = Discounter(0.04, 1 / 8760, periods) +``` """ struct Discounter discount_rate::Any timeunit_to_year::Any ts::TimeStructure end - -Discounter(rate, ts) = Discounter(rate, 1.0, ts) +Discounter(discount_rate, ts::TimeStructure) = Discounter(discount_rate, 1.0, ts) _start_strat(sp::AbstractStrategicPeriod, ts::TimeStructure{T}) where {T} = zero(T) - function _start_strat(sp::AbstractStrategicPeriod, ts::TwoLevel{S}) where {S} return sum(duration_strat(spp) for spp in strat_periods(ts) if spp < sp; init = zero(S)) end - function _start_strat(sp::StratNode, ts::TwoLevelTree{S}) where {S} start = zero(S) node = _parent(sp) @@ -27,57 +39,80 @@ function _start_strat(sp::StratNode, ts::TwoLevelTree{S}) where {S} return start end -function _sp_period(t::TimePeriod, ts::TimeStructure) - for sp in strat_periods(ts) - if _strat_per(sp) == _strat_per(t) - return sp - end - end - @error("Time period not part of any strategic period") +_sp_period(sp::AbstractStrategicPeriod, _::TimeStructure) = sp +function _sp_period(t::DiscPeriods, ts::TimeStructure) + sps = collect(strat_periods(ts)) + per = findfirst(sp -> _strat_per(sp) == _strat_per(t), sps) + isnothing(per) && throw(ErrorException("Time period not part of any strategic period")) + return sps[per] end - function _sp_period(t::TreePeriod, tree::TwoLevelTree) - for sp in strat_periods(tree) - if _strat_per(sp) == _strat_per(t) && _branch(sp) == _branch(t) - return sp - end - end - @error("Tree period not part of any strategic node") + sps = collect(strat_periods(tree)) + per = findfirst(sp -> _strat_per(sp) == _strat_per(t) && _branch(sp) == _branch(t), sps) + isnothing(per) && throw(ErrorException("Tree period not part of any strategic node")) + return sps[per] end -_start_strat(t::TimePeriod, ts::TimeStructure) = _start_strat(_sp_period(t, ts), ts) - function _to_year(start, timeunit_to_year) return start * timeunit_to_year end """ - discount(t, time_struct, discount_rate; type, timeunit_to_year) - -Calculates the discount factor to be used for a time period `t` -using a fixed 'discount_rate`. There are two types of discounting -available, either discounting to the start of the strategic period -containing the time period (`type="start"`) or calculating an approximate -value for the average discount factor over the whole strategic period. -The average can be calculated either as a continuous average (`type="avg"`) or -as a discrete average that discounts to the start of each year (`type="avg_year"`). -The `timeunit_to_year` parameter is used to convert the time units of -strategic periods in the time structure to years (default value = 1.0). + discount(t::Union{TimePeriod,TimeStructurePeriod}, time_struct::TimeStructure, discount_rate; type = "start", timeunit_to_year=1.0) + discount(disc::Discounter, t::Union{TimePeriod,TimeStructurePeriod}; type = "start") + +Calculates the discount factor to be used for a time period `t` using a fixed `discount_rate`. +The function can be either called using a [`Discounter`](@ref) type or by specifying the +parameters (time structure `ts`, `discount_rate` and potentially `timeunit_to_year`) directly. + +There are two types of discounting available: + +1. Discounting to the start of the strategic period containing the time period:\n + This can be achieved through specifying `type="start"`. It is useful for investment costs +2. Discounting to the average of the over the whole strategic period:\n + The average can be calculated either as a continuous average (`type="avg"`) or as a + discrete average that discounts to the start of each year (`type="avg_year"`). Average + discounting is useful for operational costs. + +The `timeunit_to_year` parameter is used to convert the time units of strategic periods in +the time structure to years (default value = 1.0). + +!!! tip "Comparison with `objective_weight`" + Both [`objective_weight`](@ref) and `discount` can serve similar purposes. Compared to + [`objective_weight`](@ref), `discount` only calculates the discount factor for a given + time period. If `t` is an [`AbstractStrategicPeriod`](@ref), both are + equivalent. """ function discount( - t::TimePeriod, + t::Union{TimePeriod,TimeStructurePeriod}, ts::TimeStructure, discount_rate; type = "start", timeunit_to_year = 1.0, ) sp = _sp_period(t, ts) - - return discount(sp, ts, discount_rate; type, timeunit_to_year) + discount_factor = 1.0 + if discount_rate > 0 + start_year = _to_year(_start_strat(sp, ts), timeunit_to_year) + duration_years = _to_year(duration_strat(sp), timeunit_to_year) + if type == "start" + discount_factor = discount_start(discount_rate, start_year) + elseif type == "avg" + discount_factor = discount_avg(discount_rate, start_year, duration_years) + elseif type == "avg_year" + discount_factor = discount_avg_year(discount_rate, start_year, duration_years) + end + end + return discount_factor end - -function discount(disc::Discounter, t::TimePeriod; type = "start", timeunit_to_year = 1.0) - return discount(t, disc.ts, disc.discount_rate; type, timeunit_to_year) +function discount( + disc::Discounter, + t::Union{TimePeriod,TimeStructurePeriod}; + type = "start", +) + ts = disc.ts + timeunit_to_year = disc.timeunit_to_year + return discount(t, ts, disc.discount_rate; type, timeunit_to_year) end function discount_avg(discount_rate, start_year, duration_years) @@ -96,80 +131,69 @@ function discount_start(discount_rate, start_year) return δ^start_year end -function discount( - sp::AbstractStrategicPeriod, +""" + objective_weight(t::Union{TimePeriod,TimeStructurePeriod}, ts::TimeStructure, discount_rate; type = "start", timeunit_to_year = 1.0) + objective_weight(t::Union{TimePeriod,TimeStructurePeriod}, disc::Discounter; type = "start") + + +Calculates the overall objective weight for a time period `t` using a fixed `discount_rate`. +The weight consideres both discounting, the probability and potential multiplicity of `t` +The function can be either called using a [`Discounter`](@ref) type or by specifying the +parameters (time structure `ts`, `discount_rate` and potentially `timeunit_to_year`) directly. + +There are two types of discounting available: + +1. Discounting to the start of the strategic period containing the time period:\n + This can be achieved through specifying `type="start"`. It is useful for investment costs +2. Discounting to the average of the over the whole strategic period:\n + The average can be calculated either as a continuous average (`type="avg"`) or as a + discrete average that discounts to the start of each year (`type="avg_year"`). Average + discounting is useful for operational costs. + +The `timeunit_to_year` parameter is used to convert the time units of strategic periods in +the time structure to years (default value = 1.0). + +!!! tip "Comparison with `discount`" + Both [`discount`](@ref) and `objective_weight` can serve similar purposes. Compared to + [`discount`](@ref), `objective_weight` includes as well the probablity and multiplicity + of a given time period. If `t` is an [`AbstractStrategicPeriod`](@ref), both are + equivalent. +""" +function objective_weight( + t::Union{TimePeriod,TimeStructurePeriod}, ts::TimeStructure, discount_rate; type = "start", timeunit_to_year = 1.0, ) - discount_factor = 1.0 - if discount_rate > 0 - start_year = _to_year(_start_strat(sp, ts), timeunit_to_year) - duration_years = _to_year(duration_strat(sp), timeunit_to_year) - if type == "start" - discount_factor = discount_start(discount_rate, start_year) - elseif type == "avg" - discount_factor = discount_avg(discount_rate, start_year, duration_years) - elseif type == "avg_year" - discount_factor = discount_avg_year(discount_rate, start_year, duration_years) - end - end - return discount_factor + return _objective_value(t, ts, discount_rate, type, timeunit_to_year) end - -""" - objective_weight(t, time_struct, discount_rate; type, timeunit_to_year) - -Returns an overall weight to be used for a time period `t` -in the objective function considering both discounting, -probability and possible multiplicity. There are two types of discounting -available, either discounting to the start of the strategic period -containing the time period (`type="start"`) or calculating an approximate -value for the average discount factor over the whole strategic period. -The average can be calculated either as a continuous average (`type="avg"`) or -as a discrete average that discounts to the start of each year (`type="avg_year"`). -The `timeunit_to_year` parameter is used to convert the time units of -strategic periods in the time structure to years (default value = 1.0). -""" function objective_weight( - t::TimePeriod, - ts::TimeStructure, - discount_rate; + t::Union{TimePeriod,TimeStructurePeriod}, + disc::Discounter; type = "start", - timeunit_to_year = 1.0, +) + timeunit_to_year = disc.timeunit_to_year + return objective_weight(t, disc.ts, disc.discount_rate; type = type, timeunit_to_year) +end + +function _objective_value( + t::DiscPeriods, + ts::TimeStructure, + discount_rate, + type, + timeunit_to_year, ) return probability(t) * discount(t, ts, discount_rate; type, timeunit_to_year) * multiple(t) end - -function objective_weight(t::TimePeriod, disc::Discounter; type = "start") - return objective_weight( - t, - disc.ts, - disc.discount_rate; - type = type, - timeunit_to_year = disc.timeunit_to_year, - ) -end - -function objective_weight( - sp::AbstractStrategicPeriod, +function _objective_value( + t::AbstractStrategicPeriod, ts::TimeStructure, - discount_rate; - type = "start", - timeunit_to_year = 1.0, + discount_rate, + type, + timeunit_to_year, ) - return discount(sp, ts, discount_rate; type, timeunit_to_year) -end - -function objective_weight(sp::AbstractStrategicPeriod, disc::Discounter; type = "start") - return objective_weight( - sp, - disc.ts, - disc.discount_rate; - type = type, - timeunit_to_year = disc.timeunit_to_year, - ) + return discount(t, ts, discount_rate; type, timeunit_to_year) end diff --git a/test/runtests.jl b/test/runtests.jl index 565751c..26904d2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1350,6 +1350,11 @@ end sp in strat_periods(periods) ) ≈ 4.825 atol = 1e-3 + @test sum( + objective_weight(t, periods, 0.04; timeunit_to_year = 1 / 8760, type = "start") / + (length(uniform_day) * probability(t) * multiple(t)) for t in periods + ) ≈ 4.825 atol = 1e-3 + @test sum( objective_weight(sp, periods, 0.04; timeunit_to_year = 1 / 8760, type = "avg_year") for sp in strat_periods(periods) @@ -1360,6 +1365,23 @@ end sp in strat_periods(periods) ) ≈ 4.382 atol = 1e-3 + oscs = OperationalScenarios(2, uniform_day) + periods = TwoLevel(10, 5 * 8760, oscs) + + @test sum( + objective_weight(t, periods, 0.04; timeunit_to_year = 1 / 8760, type = "start") / + (length(oscs) * probability(t) * multiple(t)) for t in periods + ) ≈ 4.825 atol = 1e-3 + oscs + + rps = RepresentativePeriods(2, 1, uniform_day) + periods = TwoLevel(10, 5 * 8760, rps) + + @test sum( + objective_weight(t, periods, 0.04; timeunit_to_year = 1 / 8760, type = "start") / + (length(rps) * probability(t) * multiple(t)) for t in periods + ) ≈ 4.825 atol = 1e-3 + uniform_day = SimpleTimes(24, 1u"hr") periods_unit = TwoLevel(10, 365.125u"d", uniform_day) @@ -1384,6 +1406,8 @@ end discount(t, periods, 0.05; type = "avg") end end + per = TimeStruct.OperationalPeriod(5, TimeStruct.SimplePeriod(1, 1), 1.0) + @test_throws ErrorException TimeStruct._sp_period(per, periods) tree = regular_tree(5, [2, 3], SimpleTimes(7, 1); op_per_strat = 365) for sp in strat_periods(tree) @@ -1397,6 +1421,8 @@ end discount(t, tree, 0.05; type = "avg") end end + per = TimeStruct.TreePeriod(2, 3, TimeStruct.SimplePeriod(1, 1), 1.0, 1.0) + @test_throws ErrorException TimeStruct._sp_period(per, tree) end @testitem "Start and end times" begin