From 0774c4d6ae55fadea8560215859b6f7e18675116 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Thu, 9 Oct 2025 15:18:01 +0200 Subject: [PATCH 1/8] Fixed the docstrings to be consistent --- src/op_scenarios/core_types.jl | 5 ++++- src/representative/core_types.jl | 6 ++++-- src/strat_scenarios/core_types.jl | 6 +++--- src/strategic/core_types.jl | 9 +++++++-- 4 files changed, 18 insertions(+), 8 deletions(-) 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..0192f84 100644 --- a/src/strat_scenarios/core_types.jl +++ b/src/strat_scenarios/core_types.jl @@ -105,7 +105,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. """ @@ -162,8 +162,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 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) From 3020a0fd0b0b6d2674302f28fe23f1a1f45ae2cc Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Thu, 9 Oct 2025 15:41:13 +0200 Subject: [PATCH 2/8] Addd TreeNode type for creation of TwoLevelTree --- src/TimeStruct.jl | 2 + src/strat_scenarios/tree_periods.jl | 121 ++++++++++++++++++++++++++++ test/runtests.jl | 50 ++++++++++++ 3 files changed, 173 insertions(+) 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/strat_scenarios/tree_periods.jl b/src/strat_scenarios/tree_periods.jl index 17940d8..4f42582 100644 --- a/src/strat_scenarios/tree_periods.jl +++ b/src/strat_scenarios/tree_periods.jl @@ -71,3 +71,124 @@ 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 diff --git a/test/runtests.jl b/test/runtests.jl index 26904d2..6f3eb44 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -917,6 +917,56 @@ 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 + # Function for testing all iterators. Can be beneficial for all other tests as well @testmodule TwoLevelTreeTest begin using TimeStruct, Test From 6b0755ed5d8e07576b094f7080ce7cea36648ce8 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Fri, 10 Oct 2025 09:00:16 +0200 Subject: [PATCH 3/8] Utilize TreeNode for TwoLevelTree construction --- src/strat_scenarios/core_types.jl | 164 +++++++++++++++++++--------- src/strat_scenarios/tree_periods.jl | 3 + src/utils.jl | 33 +++++- test/runtests.jl | 59 ++++++++-- 4 files changed, 190 insertions(+), 69 deletions(-) diff --git a/src/strat_scenarios/core_types.jl b/src/strat_scenarios/core_types.jl index 0192f84..dccf75e 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,14 +53,29 @@ 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 ∈ 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 ∈ reverse(branching) + node = TreeNode(duration, ts, k, node) + end + return TwoLevelTree(node; op_per_strat) end + function _multiple_adj(itr::TwoLevelTree, n) mult = itr.nodes[n].duration * itr.op_per_strat / _total_duration(itr.nodes[n].operational) @@ -171,49 +227,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}( +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 +276,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 +300,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 = float(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 4f42582..b4ca1d8 100644 --- a/src/strat_scenarios/tree_periods.jl +++ b/src/strat_scenarios/tree_periods.jl @@ -192,3 +192,6 @@ function 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/utils.jl b/src/utils.jl index e42c6e0..d1e6e00 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,13 @@ 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 6f3eb44..4967e06 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))), @@ -967,6 +967,43 @@ end @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 @@ -1058,7 +1095,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) @@ -1108,7 +1145,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 @@ -1117,7 +1154,7 @@ end @testitem "TwoLevelTree with RepresentativePeriods" setup = [TwoLevelTreeTest] begin regtree = - TimeStruct.regular_tree(5, [3, 2], RepresentativePeriods(2, 1, SimpleTimes(5, 1))) + TwoLevelTree(5, [3, 2], RepresentativePeriods(2, 1, SimpleTimes(5, 1))) n_sp = 10 n_rp = n_sp * 2 n_op = n_rp * 5 @@ -1126,7 +1163,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))), @@ -1139,7 +1176,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 @@ -1320,7 +1357,7 @@ 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 @@ -1459,7 +1496,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) @@ -1513,7 +1550,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 From 817fc191e7623d3e6193f4b9345c178b66620e64 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Fri, 10 Oct 2025 09:31:01 +0200 Subject: [PATCH 4/8] Updated the documentation --- docs/src/manual/multi.md | 53 ++++++++++++++++++++----------- docs/src/reference/api.md | 1 + src/strat_scenarios/core_types.jl | 3 +- 3 files changed, 38 insertions(+), 19 deletions(-) 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/strat_scenarios/core_types.jl b/src/strat_scenarios/core_types.jl index dccf75e..947a15c 100644 --- a/src/strat_scenarios/core_types.jl +++ b/src/strat_scenarios/core_types.jl @@ -238,6 +238,7 @@ end Iterative addition of a `TreeNode` `node` to a `Vector{<:StratNode}` . """ +# Ignored docstring, just fyi in this case function add_node!( nodes::Vector{<:StratNode}, node::TreeNode{S, T, OP, U}, @@ -300,7 +301,7 @@ function regular_tree( ts::OP; op_per_strat::Real = 1.0, ) where {S,T,OP<:TimeStructure{T}} - op_per_strat = float(op_per_strat) + op_per_strat = convert(Float64, op_per_strat) return TwoLevelTree(duration, branching, ts; op_per_strat) end From dec82cf12c11166380883adaac8fb338e1f96f72 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Fri, 10 Oct 2025 09:37:29 +0200 Subject: [PATCH 5/8] Applied JuliaFormatter --- src/strat_scenarios/core_types.jl | 10 ++++------ src/utils.jl | 1 - test/runtests.jl | 29 ++++++++++++++++------------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/strat_scenarios/core_types.jl b/src/strat_scenarios/core_types.jl index 947a15c..a10d0ef 100644 --- a/src/strat_scenarios/core_types.jl +++ b/src/strat_scenarios/core_types.jl @@ -53,12 +53,12 @@ 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) +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 ∈ nodes]) + len = maximum([_strat_per(sn) for sn in nodes]) return TwoLevelTree(len, nodes[1], nodes, op_per_strat) end @@ -69,13 +69,12 @@ function TwoLevelTree( op_per_strat::Float64 = 1.0, ) where {S,T,OP<:TimeStructure{T}} node = TreeNode(duration, ts) - for k ∈ reverse(branching) + 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) mult = itr.nodes[n].duration * itr.op_per_strat / _total_duration(itr.nodes[n].operational) @@ -241,13 +240,12 @@ Iterative addition of a `TreeNode` `node` to a `Vector{<:StratNode}` . # Ignored docstring, just fyi in this case function add_node!( nodes::Vector{<:StratNode}, - node::TreeNode{S, T, OP, U}, + 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, diff --git a/src/utils.jl b/src/utils.jl index d1e6e00..b47df38 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -166,7 +166,6 @@ 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.", diff --git a/test/runtests.jl b/test/runtests.jl index 4967e06..cf7fb45 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -978,18 +978,22 @@ end # 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, + [ + 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)), + ], ) - 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 @@ -1153,8 +1157,7 @@ end end @testitem "TwoLevelTree with RepresentativePeriods" setup = [TwoLevelTreeTest] begin - regtree = - TwoLevelTree(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 From a71b742426af4a65454db85baf142ece3c03a29c Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Fri, 10 Oct 2025 10:12:20 +0200 Subject: [PATCH 6/8] Added missing tests and fixed formatting manually --- src/strat_scenarios/core_types.jl | 2 +- test/runtests.jl | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/strat_scenarios/core_types.jl b/src/strat_scenarios/core_types.jl index a10d0ef..441754b 100644 --- a/src/strat_scenarios/core_types.jl +++ b/src/strat_scenarios/core_types.jl @@ -265,7 +265,7 @@ function add_node!( # Add the new node total_prob = prob * sub_prob - add_node!(nodes, sub_tn, new_node, total_prob, sp+1, op_per_strat) + add_node!(nodes, sub_tn, new_node, total_prob, sp + 1, op_per_strat) end end diff --git a/test/runtests.jl b/test/runtests.jl index cf7fb45..646233a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -939,13 +939,13 @@ end 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.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.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) @@ -1367,6 +1367,8 @@ end end @test_throws ErrorException withnext(strat_periods(two_level_tree)) + @test_throws ErrorException chunk(strat_periods(two_level_tree)) + @test_throws ErrorException chunk_duration(strat_periods(two_level_tree)) periods = SimpleTimes(10, 1) From 95fbc4edac9e1c653aa163931eeb857f9e423f2c Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Fri, 10 Oct 2025 10:21:00 +0200 Subject: [PATCH 7/8] Fixed error in tests and coumentation --- src/strat_scenarios/core_types.jl | 6 ++++-- test/runtests.jl | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/strat_scenarios/core_types.jl b/src/strat_scenarios/core_types.jl index 441754b..dfec6ea 100644 --- a/src/strat_scenarios/core_types.jl +++ b/src/strat_scenarios/core_types.jl @@ -190,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 the `TwoLevle` as 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 diff --git a/test/runtests.jl b/test/runtests.jl index 646233a..397c68c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1367,8 +1367,8 @@ end end @test_throws ErrorException withnext(strat_periods(two_level_tree)) - @test_throws ErrorException chunk(strat_periods(two_level_tree)) - @test_throws ErrorException chunk_duration(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) From 15871645fb23d97e957cf5c1725f55283e52bd59 Mon Sep 17 00:00:00 2001 From: Julian Straus <104911227+JulStraus@users.noreply.github.com> Date: Fri, 10 Oct 2025 11:00:25 +0200 Subject: [PATCH 8/8] Update src/strat_scenarios/core_types.jl Co-authored-by: Truls Flatberg <75753981+trulsf@users.noreply.github.com> --- src/strat_scenarios/core_types.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strat_scenarios/core_types.jl b/src/strat_scenarios/core_types.jl index dfec6ea..7003a96 100644 --- a/src/strat_scenarios/core_types.jl +++ b/src/strat_scenarios/core_types.jl @@ -198,7 +198,7 @@ This function returns a type for iterating through the individual strategic scen input `TimeStructure`. When the `TimeStructure` is a [`TwoLevel`](@ref), `strategic_scenarios` returns a Vector with -the the `TwoLevle` as single entry. +the `TwoLevel` as a single entry. """ strategic_scenarios(ts::TwoLevel) = [ts]