diff --git a/docs/src/manual/multi.md b/docs/src/manual/multi.md index 14c925d..3592de1 100644 --- a/docs/src/manual/multi.md +++ b/docs/src/manual/multi.md @@ -73,30 +73,46 @@ If there is uncertainty at a strategic level, this can be incorporated using the time structure. This structure is represented by a tree, with each node corresponding to a strategic period that contains an operational time structure. The operational time structure can be any combination of the *[described structures](@ref man-oper)*. -The following example demonstrates how to create a regular tree (through the function [`regular_tree`](@ref)) -where each strategic period spans 3 years and is represented by a week with daily resolution. -The second argument to the [`regular_tree`](@ref) function specifies the number -of branches at each stage of the tree, excluding the first stage. +The following example demonstrates how to create a regular tree through a constructor (`TwoLevelTree(duration::S, branching::Vector, ts::OP; op_per_strat::Float64 = 1.0) where {S,T,OP<:TimeStructure{T}}`) where each strategic period spans 3 years and is represented by a week with daily resolution. +The second argument of the constructor function specifies the number of branches at each stage of the tree, excluding the first stage. + ```@repl ts using TimeStruct -operational = SimpleTimes(7, 1); -two_level_tree = regular_tree(3, [3,2], operational; op_per_strat = 52); +week = SimpleTimes(7, 1); +two_level_tree = TwoLevelTree(3, [3, 2], week; op_per_strat = 52.0); ``` ![Illustration of TwoLevelTree](./../figures/two_level_tree.png) The branching probabilities are equal for all branches as indicated in green in the figure. -!!! note "Constructors for TwoLevelTree" - Currently, the functionality for creating `TwoLevelTree`'s is limited. Future versions of the package - will expand this functionality to allow creating trees with varying probabilities and different operational - time structures for the nodes. +We also provide the possibility of having differing tree structures through the application of the [`TreeNode`](@ref) type. +A [`TreeNode`](@ref) approach for above's time structure would be given by +```@repl ts +using TimeStruct +week = SimpleTimes(7, 1); +two_level_tree = TwoLevelTree( + TreeNode(3, week, [ + TreeNode(3, week, 2, + TreeNode(3, week) + ) + TreeNode(3, week, [0.5, 0.5], + TreeNode(3, week) + ) + TreeNode(3, week, [ + TreeNode(3, week), + TreeNode(3, week), + ]) + ]); + op_per_strat = 52.0 +) +``` + +Similar as for [`TwoLevel`](@ref), the strategic nodes can be iterated using [`strat_periods`](@ref). +It is possible to connect the nodes to their predecessor by iterating using the [`withprev`](@ref) iterator that returns a tuple with the parent or nothing if no parent, together with the node itself. +This provides the flexibility to track decisions in the tree as shown by the following example that allows investment into new capacity in each strategic node while tracking the accumulated capacity. -Similar as for [`TwoLevel`](@ref), the strategic nodes can be iterated using [`strat_periods`](@ref). It is possible to connect the nodes to their predecessor by -iterating using the [`withprev`](@ref) iterator that returns a tuple with the parent or nothing if no parent, together with the node itself. This provides -the flexibility to track decisions in the tree as shown by the following example that allows investment into new capacity in each strategic node -while tracking the accumulated capacity. ```@repl ts using JuMP @@ -110,10 +126,11 @@ for (prev, sp) in withprev(strat_pers) end end ``` -To ensure consistency across the tree, it is possible to iterate through all strategic scenarios -in the tree using [`strategic_scenarios`](@ref). Here each scenario is a path from the root node -to one of the leaves of the tree. In the example above, if we only allow one investment in the -planning period, this can be added by restricting the number of investments in each scenario: + +To ensure consistency across the tree, it is possible to iterate through all strategic scenarios in the tree using [`strategic_scenarios`](@ref). +Here each scenario is a path from the root node to one of the leaves of the tree. +In the example above, if we only allow one investment in the planning period, this can be added by restricting the number of investments in each scenario: + ```@repl ts for sc in strategic_scenarios(two_level_tree) @constraint(m, sum(invest[sp] for sp in sc) <= 1) diff --git a/docs/src/reference/api.md b/docs/src/reference/api.md index 3abcc5b..ed89c9e 100644 --- a/docs/src/reference/api.md +++ b/docs/src/reference/api.md @@ -17,6 +17,7 @@ OperationalScenarios RepresentativePeriods TwoLevel TwoLevelTree +TreeNode regular_tree ``` diff --git a/src/TimeStruct.jl b/src/TimeStruct.jl index 9a1ec18..b190f20 100644 --- a/src/TimeStruct.jl +++ b/src/TimeStruct.jl @@ -34,6 +34,8 @@ export RepresentativePeriods export TwoLevel export TwoLevelTree +export TreeNode + export TimeProfile export FixedProfile export OperationalProfile diff --git a/src/op_scenarios/core_types.jl b/src/op_scenarios/core_types.jl index 2822528..9e7d923 100644 --- a/src/op_scenarios/core_types.jl +++ b/src/op_scenarios/core_types.jl @@ -12,7 +12,8 @@ and an associated probability. These scenarios are in general represented as [`SimpleTimes`](@ref). !!! note - - All scenarios must use the same type for the duration, _.i.e._, either Integer or Float. + - The `TimeStructure`s of all operational scenarios must use the same type for the + duration, *.i.e.*, either Integer or Float. - If the `probability` is not specified, it assigns the same probability to each scenario. - It is possible that `sum(probability)` is larger or smaller than 1. This can lead to problems in your application. Hence, it is advised to scale it. Currently, a warning @@ -20,8 +21,10 @@ and an associated probability. These scenarios are in general represented as correspond to a breaking change. ## Example + The following examples create a time structure with 2 operational scenarios corresponding to a single day with equal probability. + ```julia day = SimpleTimes(24, 1) OperationalScenarios(2, day) diff --git a/src/representative/core_types.jl b/src/representative/core_types.jl index 8ab598d..4e01fb2 100644 --- a/src/representative/core_types.jl +++ b/src/representative/core_types.jl @@ -19,7 +19,8 @@ has an associated share that specifies how much of the total duration that is attributed to it. !!! note - - All representative periods must use the same type for the `TimeStructure`. + - The `TimeStructure`s of all representative periods must use the same type for the + duration, *.i.e.*, either Integer or Float. - If the field `period_share` is not specified, it assigns the same probability to each representative period. - It is possible that `sum(period_share)` is larger or smaller than 1. This can lead to @@ -29,7 +30,8 @@ is attributed to it. - If you include [`OperationalScenarios`](@ref) in your time structure, it is important that the scenarios are within the representative periods, and not the other way. -### Example +## Example + ```julia # A year represented by two days with hourly resolution and relative shares of 0.7 and 0.3 RepresentativePeriods(8760, [0.7, 0.3], [SimpleTimes(24, 1), SimpleTimes(24,1)]) diff --git a/src/strat_scenarios/core_types.jl b/src/strat_scenarios/core_types.jl index db391d7..7003a96 100644 --- a/src/strat_scenarios/core_types.jl +++ b/src/strat_scenarios/core_types.jl @@ -1,10 +1,51 @@ """ mutable struct TwoLevelTree{S,T,OP<:AbstractTreeNode{S,T}} <: TimeStructure{T} -Time structure allowing for a tree structure for the strategic level. - -For each strategic node in the tree a separate time structure is used for -operational decisions. Iterating the structure will go through all operational periods. + TwoLevelTree(node::TreeNode; op_per_strat=8760.0) + TwoLevelTree(duration::S, branching::Vector, ts::OP; op_per_strat::Float64 = 1.0) where {S,T,OP<:TimeStructure{T}} + +Time structure allowing for a tree structure for the strategic level. For each strategic +node in the tree a separate time structure is used for operational decisions. Iterating the +structure will go through all operational periods. + +The default approach for creating a `TwoLevelTree` is by providing the root `[TreeNode`](@ref) +with all its children nodes. In the case of a regular structure, that is all children nodes +have the same `duration`, time structure `ts`, probability, and children itself, you can use +a simplified constructor with the `branching` vector. The vector `branching` specifies the +number of branchings at each stage of the tree, excluding the first stage. The branches at +each stage will all have equal probability, duration, and time structure. + +!!! warning "Additional iteratores" + `TwoLevelTree` utilize a separate [`withprev`](@ref) method which is equivalent to the + existing method for the other time structures. [`withnext`](@ref), [`chunk](@ref) and + [`chunk_duration`](@ref) are not implemented and will result in an error when used. + +## Example + +```julia +# Declare the individual time structure +day = SimpleTimes(24, 1) + +# Regular tree with 3 strategic periods of duration 5, 3 branches for the second strategic +# period, and 6 branchs in the thirdand forth strategic period +regtree_1 = TwoLevelTree(5, [3, 2, 1], day) + +# Equivalent structure using `TreeNode` and the different constructors +regtree_2 = TwoLevelTree( + TreeNode(5, day, [ + TreeNode(5, day, 2, + TreeNode(5, day, TreeNode(5, day)) + ) + TreeNode(5, day, [0.5, 0.5], + TreeNode(5, day, TreeNode(5, day)) + ) + TreeNode(5, day, [ + TreeNode(5, day, TreeNode(5, day)), + TreeNode(5, day, TreeNode(5, day)), + ]) + ]) +) +``` """ mutable struct TwoLevelTree{S,T,OP<:AbstractTreeNode{S,T}} <: TimeStructure{T} len::Int @@ -12,12 +53,26 @@ mutable struct TwoLevelTree{S,T,OP<:AbstractTreeNode{S,T}} <: TimeStructure{T} nodes::Vector{OP} op_per_strat::Float64 end +function TwoLevelTree(parent::TreeNode; op_per_strat = 1.0) + nodes = StratNode[] + + add_node!(nodes, parent, nothing, 1.0, 1, op_per_strat) + nodes = convert(Array{typejoin(typeof.(nodes)...)}, nodes) + len = maximum([_strat_per(sn) for sn in nodes]) -function TwoLevelTree{S,T,OP}( - nodes::Vector{OP}, - op_per_strat, -) where {S,T,OP<:AbstractTreeNode{S,T}} - return TwoLevelTree{S,T,OP}(0, nothing, nodes, op_per_strat) + return TwoLevelTree(len, nodes[1], nodes, op_per_strat) +end +function TwoLevelTree( + duration::S, + branching::Vector, + ts::OP; + op_per_strat::Float64 = 1.0, +) where {S,T,OP<:TimeStructure{T}} + node = TreeNode(duration, ts) + for k in reverse(branching) + node = TreeNode(duration, ts, k, node) + end + return TwoLevelTree(node; op_per_strat) end function _multiple_adj(itr::TwoLevelTree, n) @@ -105,7 +160,7 @@ end """ struct StrategicScenario -Desription of an individual strategic scenario. It includes all strategic nodes +Description of an individual strategic scenario. It includes all strategic nodes corresponding to a scenario, including the probability. It can be utilized within a decomposition algorithm. """ @@ -135,15 +190,17 @@ struct StrategicScenarios end """ + strategic_scenarios(ts::TwoLevel) strategic_scenarios(ts::TwoLevelTree) This function returns a type for iterating through the individual strategic scenarios of a `TwoLevelTree`. The type of the iterator is dependent on the type of the input `TimeStructure`. -When the `TimeStructure` is a `TimeStructure`, `strategic_scenarios` returns a +When the `TimeStructure` is a [`TwoLevel`](@ref), `strategic_scenarios` returns a Vector with +the `TwoLevel` as a single entry. """ -strategic_scenarios(two_level::TwoLevel) = [two_level] +strategic_scenarios(ts::TwoLevel) = [ts] """ When the `TimeStructure` is a [`TwoLevelTree`](@ref), `strategic_scenarios` returns the @@ -162,8 +219,8 @@ function Base.iterate(scs::StrategicScenarios, state = 1) node = getleaf(scs.ts, state) prob = probability_branch(node) nodes = [node] - while !isnothing(node.parent) - node = node.parent + while !isnothing(_parent(node)) + node = _parent(node) pushfirst!(nodes, node) end @@ -171,49 +228,46 @@ function Base.iterate(scs::StrategicScenarios, state = 1) end """ - add_node( - tree::TwoLevelTree{T, StratNode{S, T, OP}}, - parent, - sp, - duration::S, - branch_prob, - branching, - oper::OP, - ) where {S, T, OP<:TimeStructure{T}} - -Iterative addition of nodes. + add_node!( + nodes::Vector{<:StratNode}, + node::TreeNode{S, T, OP, U}, + parent::Union{Nothing,StratNode}, + prob::Float64, + sp::Int64, + op_per_strat::Real, + ) where {S,T,OP<:TimeStructure{T},U} + +Iterative addition of a `TreeNode` `node` to a `Vector{<:StratNode}` . """ -# Add nodes iteratively in a depth first manner -function add_node( - tree::TwoLevelTree{S,T,StratNode{S,T,OP}}, - parent, - sp, - duration::S, - branch_prob, - branching, - oper::OP, -) where {S,T,OP<:TimeStructure{T}} - prob_branch = branch_prob * (isnothing(parent) ? 1.0 : parent.prob_branch) - mult_sp = duration * tree.op_per_strat / _total_duration(oper) - node = StratNode{S,T,OP}( +# Ignored docstring, just fyi in this case +function add_node!( + nodes::Vector{<:StratNode}, + node::TreeNode{S,T,OP,U}, + parent::Union{Nothing,StratNode}, + prob::Float64, + sp::Int64, + op_per_strat::Real, +) where {S,T,OP<:TimeStructure{T},U} + oper = node.ts + new_node = StratNode( sp, - branches(tree, sp) + 1, - duration, - mult_sp, - prob_branch, + count(n -> _strat_per(n) == sp, nodes) + 1, + duration_strat(node), + duration_strat(node) * op_per_strat / _total_duration(oper), + prob, parent, oper, ) - push!(tree.nodes, node) - if isnothing(parent) - tree.root = node - end + push!(nodes, new_node) - if sp < tree.len - for i in 1:branching[sp] - # TODO: consider branching probability as input, but use uniform for now - add_node(tree, node, sp + 1, duration, 1.0 / branching[sp], branching, oper) - end + # Iterate through the children and add their nodes + for (sub_prob, sub_tn) in zip(node.probability, children(node)) + # Continue when reaching leaf node + isnothing(sub_tn) && continue + + # Add the new node + total_prob = prob * sub_prob + add_node!(nodes, sub_tn, new_node, total_prob, sp + 1, op_per_strat) end end @@ -223,13 +277,23 @@ end branching::Vector, ts::OP; op_per_strat::Real=1.0, - ) where {S, T, OP<:TimeStructure{T}} + ) where {S,T,OP<:TimeStructure{T}} Function for creating a regular tree with a uniform structure for each strategic period. + Each strategic period is of equal length as given by `duration` and will have the same operational time structure `ts`. The vector `branching` specifies the number of branchings at each stage of the tree, excluding the first stage. The branches at each stage will all have equal probability. + +!!! note "Deprecated function" + This function is deprecated and will be removed in a later release. The new function is + given by + + ```julia + TwoLevelTree(duration, branching, ts; op_per_strat) + ``` + """ function regular_tree( duration::S, @@ -237,11 +301,8 @@ function regular_tree( ts::OP; op_per_strat::Real = 1.0, ) where {S,T,OP<:TimeStructure{T}} - tree = TwoLevelTree{S,T,StratNode{S,T,OP}}(Vector{StratNode{S,T,OP}}(), op_per_strat) - tree.len = length(branching) + 1 - add_node(tree, nothing, 1, duration, 1.0, branching, ts) - - return tree + op_per_strat = convert(Float64, op_per_strat) + return TwoLevelTree(duration, branching, ts; op_per_strat) end """ diff --git a/src/strat_scenarios/tree_periods.jl b/src/strat_scenarios/tree_periods.jl index 17940d8..b4ca1d8 100644 --- a/src/strat_scenarios/tree_periods.jl +++ b/src/strat_scenarios/tree_periods.jl @@ -71,3 +71,127 @@ function Base.iterate(n::StratNode, state = nothing) return TreePeriod(n, next[1]), next[2] end + +""" + struct TreeNode{S,T,OP<:TimeStructure{T},COP<:Union{Nothing, TimeStructure{T}}} + + TreeNode(duration::Number, ts::TimeStructure) + TreeNode(duration::Number, ts::TimeStructure, child::TreeNode) + TreeNode(duration::Number, ts::TimeStructure, len::Int64, children::TreeNode) + TreeNode(duration::Number, ts::TimeStructure, children::Vector{<:TreeNode}) + TreeNode(duration::Number, ts::TimeStructure, probability::Vector{<:Float64}, sub_tn::TreeNode) + +A subtype introduced for creating all potential structures of a `TwoLevelTree`. + +A `TreeNode` is similar to a [`StratNode`](@ref) but does not require the calculation of all +required parameters by the user. The parameters are automatically calculated when creating +a [`TwoLevelTree`](@ref) for a given `TreeNode`. + +The TreeNode has a given `duration` which is similar to the `duration` of a [`TwoLevel`](@ref) +time structure and a single TimeStructure `ts`. + +!!! note + - The `TimeStructure` of the `TreeNode` and all children must use the same type for the + duration, *.i.e.*, either Integer or Float. + +## Constructors + +The following constructors are included for the type: + +- `TreeNode(duration::Number, ts::TimeStructure)`: the last `TreeNode` within a branch, + *i.e.*, the leaf of the branch. It does not posess a child. +- `TreeNode(duration::Number, ts::TimeStructure, child::TreeNode)`: a `TreeNode` with only a + single child. *i.e.*, a linear continuation. +- `TreeNode(duration::Number, ts::TimeStructure, len::Int64, children::TreeNode)`: a `TreeNode` + with `len` children, each with the same structure and probability. +- `TreeNode(duration::Number, ts::TimeStructure, probability::Vector{<:Float64}, children::TreeNode)`: + a `TreeNode` with `len` children, each with the same structure but a different probability. +- `TreeNode(duration::Number, ts::TimeStructure, children::Vector{<:TreeNode})` corresponds + to a number of different children with different structures, but the same probability. +- `TreeNode(duration::Number, ts::TimeStructure, probability::Vector{<:Float64}, children::TreeNode)`: + a `TreeNode` with `length(probability)` children, each with the same structure but a + different probability. +- `TreeNode(duration::Number, ts::TimeStructure, probability::Vector{<:Float64}, children::Vector{<:TreeNode})`: + a `TreeNode` with `length(probability)` children, each with a different structure and + probability. + +## Example + +```julia +day = SimpleTimes(24, 1) +# Provides a leaf node +TreeNode(2, day) + +# Provides a linear node structure +TreeNode(2, day, TreeNode(2, day)) + +# Provides a tree node with two children with 70 % and 30 % probability, respectively +TreeNode(2, day, [0.7, 0.3], TreeNode(2, SimpleTimes(24, 1))) + + +# Provide a tree node with two children with 70 % (168 periods) and 30 % (24 periods) +# probability, respectively +TreeNode(2, day, [0.7, 0.3], [ + TreeNode(2, SimpleTimes(168, 1)), + TreeNode(2, SimpleTimes(24, 1)), +]) +``` +""" +struct TreeNode{S,T,OP<:TimeStructure{T},COP} + duration::S + ts::OP + probability::Vector{Float64} + children::Vector{COP} + function TreeNode( + duration::S, + ts::OP, + probability::Vector{Float64}, + children::Vector{COP}, + ) where {S,T,OP<:TimeStructure{T},COP} + if length(probability) ≠ length(children) + throw( + ArgumentError( + "The length of `probability` must be equal to the length of `children`.", + ), + ) + elseif !isapprox(sum(probability), 1; atol = 1e-6) + @warn( + "The sum of the probablity vector is given by $(sum(probability)). " * + "This can lead to unexpected behaviour." + ) + end + return new{S,T,OP,COP}(duration, ts, probability, children) + end +end +# Constructor for the last TreeNode in a branch, i.e., the leaf +function TreeNode(duration::Number, ts::TimeStructure) + return TreeNode(duration, ts, [1.0], [nothing]) +end +# Constructor for a case in which the children time structure does not incorporate uncertainty +function TreeNode(duration::Number, ts::TimeStructure, child::TreeNode) + return TreeNode(duration, ts, [1.0], [child]) +end +# Constructor for a case in which all children are equal (time structure, children, and so on) +# and have the same probability +function TreeNode(duration::Number, ts::TimeStructure, len::Int64, children::TreeNode) + return TreeNode(duration, ts, ones(len) ./ len, fill(children, len)) +end +# Constructor for a case in which all children have the same probability +function TreeNode(duration::Number, ts::TimeStructure, children::Vector{<:TreeNode}) + len_children = length(children) + return TreeNode(duration, ts, ones(len_children) ./ len_children, children) +end +# Constructor for a case in which all children are equal (time structure, children, and so on), +# but can have a different probability +function TreeNode( + duration::Number, + ts::TimeStructure, + probability::Vector{<:Float64}, + children::TreeNode, +) + len = length(probability) + return TreeNode(duration, ts, probability, fill(children, len)) +end + +duration_strat(n::TreeNode) = n.duration +children(n::TreeNode) = n.children diff --git a/src/strategic/core_types.jl b/src/strategic/core_types.jl index 0f496b0..2f1ad22 100644 --- a/src/strategic/core_types.jl +++ b/src/strategic/core_types.jl @@ -22,6 +22,10 @@ Potential time structures are [`SimpleTimes`](@ref), [`CalendarTimes`](@ref), [`OperationalScenarios`](@ref), or [`RepresentativePeriods`](@ref), as well as combinations of these. +!!! note + - The `TimeStructure`s of all strategic periods must use the same type for the + duration, *.i.e.*, either Integer or Float. + !!! danger "Usage of op_per_strat" The optional keyword `op_per_strat` is important for the overall calculations. If you use an hourly resolution for your operational period and yearly for investment @@ -37,7 +41,8 @@ of these. corresponds in this case to the sum of the duration of all operational periods divided by the value of the field `op_per_strat`. -Example +## Example + ```julia # 5 years with 24 hours of operations for each year. Note that in this case we use as unit # `hour` for both the duration of strategic periods and operational periods @@ -48,7 +53,7 @@ TwoLevel(5, 8760, SimpleTimes(24, 1)) TwoLevel(5, 1, SimpleTimes(24, 1); op_per_strat=8760.0) # All individual constructors -TwoLevel(2, ones(2), [SimpleTimes(24, 1), SimpleTimes(24, 1)], op_per_strat=8760.0) +TwoLevel(2, ones(2), [SimpleTimes(24, 1), SimpleTimes(24, 1)], 8760.0) TwoLevel(2, 1, SimpleTimes(24, 1); op_per_strat=8760.0) TwoLevel(1, [SimpleTimes(24, 1), SimpleTimes(24, 1)]; op_per_strat=8760.0) TwoLevel(ones(2), SimpleTimes(24, 1); op_per_strat=8760.0) diff --git a/src/utils.jl b/src/utils.jl index e42c6e0..b47df38 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -83,11 +83,15 @@ end """ chunk(iter, n; cyclic = false) -Iterator wrapper that yields chunks where each chunk is an iterator over at most -`n` consecutive time periods starting at each time period of the original iterator. +Iterator wrapper that yields chunks where each chunk is an iterator over at most `n` +consecutive time periods starting at each time period of the original iterator. -It is possible to get the `n` consecutive time periods in a cyclic fashion, by -setting `cyclic` to true. +It is possible to get the `n` consecutive time periods in a cyclic fashion, by setting +`cyclic` to true. + +!!! warning "TwoLevelTree" + Usage of the function for the strategic periods of a [`TwoLevelTree`](@ref) time + structure results in an error. """ chunk(iter, n; cyclic = false) = Chunk(iter, n, cyclic) Base.length(w::Chunk) = length(w.itr) @@ -104,6 +108,12 @@ function Base.iterate(w::Chunk, state = nothing) return next, n[2] end +function chunk(_::StratTreeNodes{S,T,OP}, _) where {S,T,OP} + return error( + "`chunk` can not be used when iterating nodes of a strategic tree structure.", + ) +end + struct TakeDuration{I} xs::I duration::Duration @@ -132,8 +142,12 @@ end """ chunk_duration(iter, dur) -Iterator wrapper that yields chunks based on duration where each chunk is an iterator over the following -time periods until at least `dur` time is covered or the end is reached. +Iterator wrapper that yields chunks based on duration where each chunk is an iterator over +the following time periods until at least `dur` time is covered or the end is reached. + +!!! warning "TwoLevelTree" + Usage of the function for the strategic periods of a [`TwoLevelTree`](@ref) time + structure results in an error. """ chunk_duration(iter, dur; cyclic = false) = ChunkDuration(iter, dur, cyclic) @@ -152,6 +166,12 @@ function Base.iterate(w::ChunkDuration, state = nothing) return next, n[2] end +function chunk_duration(_::StratTreeNodes{S,T,OP}, _) where {S,T,OP} + return error( + "`chunk_duration` can not be used when iterating nodes of a strategic tree structure.", + ) +end + """ end_oper_time(t, ts) diff --git a/test/runtests.jl b/test/runtests.jl index 26904d2..397c68c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -775,13 +775,13 @@ end twolevel_rep = TwoLevel(2, 1, rep) test_multiple(twolevel_rep) - regtree_simple = regular_tree(5, [3, 2], periods) + regtree_simple = TwoLevelTree(5, [3, 2], periods) test_multiple(regtree_simple) - regtree_opscen = regular_tree(5, [3, 2], opscen) + regtree_opscen = TwoLevelTree(5, [3, 2], opscen) test_multiple(regtree_opscen) - regtree = TimeStruct.regular_tree( + regtree = TwoLevelTree( 5, [3, 2], RepresentativePeriods(2, 1, OperationalScenarios(3, SimpleTimes(5, 1))), @@ -917,6 +917,97 @@ end @test sum(sspdiv[t] for t in tsc) == 200 / 0.5 end +@testitem "TreeNode constructors" begin + # Different structures to be used + day = SimpleTimes(24, 1) + week = SimpleTimes(168, 1) + oscs = OperationalScenarios(2, week) + rps = RepresentativePeriods(2, 1, day) + + leaf_node = TreeNode(2, day) + @test leaf_node.duration == 2 + @test leaf_node.ts == day + @test leaf_node.probability == [1] + @test leaf_node.children == [nothing] + + lin_node = TreeNode(5, week, leaf_node) + @test lin_node.duration == 5 + @test lin_node.ts == week + @test lin_node.probability == [1] + @test lin_node.children == [leaf_node] + + all_equal_node = TreeNode(10, oscs, 3, leaf_node) + @test all_equal_node.duration == 10 + @test all_equal_node.ts == oscs + @test all_equal_node.probability == [1, 1, 1] / 3 + @test all_equal_node.children == [leaf_node, leaf_node, leaf_node] + + prob_equal_node = TreeNode(20, rps, [leaf_node, lin_node]) + @test prob_equal_node.duration == 20 + @test prob_equal_node.ts == rps + @test prob_equal_node.probability == [1, 1] / 2 + @test prob_equal_node.children == [leaf_node, lin_node] + + children_equal_node = TreeNode(20, rps, [0.6, 0.4], leaf_node) + @test children_equal_node.duration == 20 + @test children_equal_node.ts == rps + @test children_equal_node.probability == [0.6, 0.4] + @test children_equal_node.children == [leaf_node, leaf_node] + + unequal_node = TreeNode(25, day, [0.3, 0.7], [leaf_node, lin_node]) + @test unequal_node.duration == 25 + @test unequal_node.ts == day + @test unequal_node.probability == [0.3, 0.7] + @test unequal_node.children == [leaf_node, lin_node] + + @test_throws ArgumentError TreeNode(25, day, [0.3], [leaf_node, lin_node]) + msg = + "The sum of the probablity vector is given by 0.8. " * + "This can lead to unexpected behaviour." + @test_logs (:warn, msg) TreeNode(25, day, [0.5, 0.3], lin_node) +end + +@testitem "TwoLevelTree constructors" begin + # Declare the individual time structure + day = SimpleTimes(24, 1) + + # Regular tree with 3 strategic periods of duration 5, 3 branches for the second strategic + # period, and 6 branchs in the thirdand forth strategic period + regtree_1 = TwoLevelTree(5, [3, 2, 1], day) + regtree_2 = regular_tree(5, [3, 2, 1], day) + + # Equivalent structure using `TreeNode` and the different constructors + regtree_3 = TwoLevelTree( + TreeNode( + 5, + day, + [ + TreeNode(5, day, 2, TreeNode(5, day, TreeNode(5, day))) + TreeNode(5, day, [0.5, 0.5], TreeNode(5, day, TreeNode(5, day))) + TreeNode( + 5, + day, + [ + TreeNode(5, day, TreeNode(5, day)), + TreeNode(5, day, TreeNode(5, day)), + ], + ) + ], + ), + ) + + # Test that they are equivalent + # The test on TwoLevelTree level is not working, even though all fields are equal + regtree_1.len == regtree_2.len + regtree_1.root == regtree_2.root + regtree_1.nodes == regtree_2.nodes + regtree_1.op_per_strat == regtree_2.op_per_strat + regtree_1.len == regtree_3.len + regtree_1.root == regtree_3.root + regtree_1.nodes == regtree_3.nodes + regtree_1.op_per_strat == regtree_3.op_per_strat +end + # Function for testing all iterators. Can be beneficial for all other tests as well @testmodule TwoLevelTreeTest begin using TimeStruct, Test @@ -1008,7 +1099,7 @@ end end @testitem "TwoLevelTree with SimpleTimes" setup = [TwoLevelTreeTest] begin - regtree = TimeStruct.regular_tree(5, [3, 2], SimpleTimes(5, 1)) + regtree = TwoLevelTree(5, [3, 2], SimpleTimes(5, 1)) n_sp = 10 n_op = n_sp * 5 ops = TwoLevelTreeTest.fun(regtree, n_sp, n_op) @@ -1058,7 +1149,7 @@ end end @testitem "TwoLevelTree with OperationalScenarios" setup = [TwoLevelTreeTest] begin - regtree = TimeStruct.regular_tree(5, [3, 2], OperationalScenarios(3, SimpleTimes(5, 1))) + regtree = TwoLevelTree(5, [3, 2], OperationalScenarios(3, SimpleTimes(5, 1))) n_sp = 10 n_sc = n_sp * 3 n_op = n_sc * 5 @@ -1066,8 +1157,7 @@ end end @testitem "TwoLevelTree with RepresentativePeriods" setup = [TwoLevelTreeTest] begin - regtree = - TimeStruct.regular_tree(5, [3, 2], RepresentativePeriods(2, 1, SimpleTimes(5, 1))) + regtree = TwoLevelTree(5, [3, 2], RepresentativePeriods(2, 1, SimpleTimes(5, 1))) n_sp = 10 n_rp = n_sp * 2 n_op = n_rp * 5 @@ -1076,7 +1166,7 @@ end @testitem "TwoLevelTree with RepresentativePeriods and OperationalScenarios" setup = [TwoLevelTreeTest] begin - regtree = TimeStruct.regular_tree( + regtree = TwoLevelTree( 5, [3, 2], RepresentativePeriods(2, 1, OperationalScenarios(3, SimpleTimes(5, 1))), @@ -1089,7 +1179,7 @@ end end @testitem "Strategic scenarios with operational scenarios" begin - regtree = TimeStruct.regular_tree(5, [3, 2], OperationalScenarios(3, SimpleTimes(5, 1))) + regtree = TwoLevelTree(5, [3, 2], OperationalScenarios(3, SimpleTimes(5, 1))) @test length(TimeStruct.strategic_scenarios(regtree)) == 6 @@ -1270,13 +1360,15 @@ end end end - two_level_tree = regular_tree(5, [3, 2], uniform_day) + two_level_tree = TwoLevelTree(5, [3, 2], uniform_day) for (prev, n) in withprev(strat_periods(two_level_tree)) @test n.parent == prev end @test_throws ErrorException withnext(strat_periods(two_level_tree)) + @test_throws ErrorException chunk(strat_periods(two_level_tree), 2) + @test_throws ErrorException chunk_duration(strat_periods(two_level_tree), 2) periods = SimpleTimes(10, 1) @@ -1409,7 +1501,7 @@ 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) + tree = TwoLevelTree(5, [2, 3], SimpleTimes(7, 1); op_per_strat = 365.0) for sp in strat_periods(tree) sp_per = strat_periods(periods)[TimeStruct._strat_per(sp)] @test discount(sp, tree, 0.05) == discount(sp_per, periods, 0.05) @@ -1463,7 +1555,7 @@ end @test end_oper_time(t, year) - start_oper_time(t, year) == duration(t) end - reg_tree = regular_tree(5, [3, 2], SimpleTimes(5, 1)) + reg_tree = TwoLevelTree(5, [3, 2], SimpleTimes(5, 1)) for t in reg_tree @test end_oper_time(t, reg_tree) - start_oper_time(t, reg_tree) == duration(t) end