diff --git a/docs/make.jl b/docs/make.jl index 21ca29c..3d61d26 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -10,8 +10,16 @@ pages = [ "Discounting" => "manual/discount.md", ], "API reference" => "reference/api.md", + "Internal reference" => "reference/internal.md", ] +DocMeta.setdocmeta!( + TimeStruct, + :DocTestSetup, + :(using TimeStruct); + recursive = true, +) + Documenter.makedocs( sitename = "TimeStruct", format = Documenter.HTML(; diff --git a/docs/src/reference/api.md b/docs/src/reference/api.md index 262a056..f74e7eb 100644 --- a/docs/src/reference/api.md +++ b/docs/src/reference/api.md @@ -1,7 +1,7 @@ + ```@meta CurrentModule = TimeStruct ``` - # Available time structures ```@docs @@ -28,13 +28,17 @@ OperationalScenarios TwoLevel ``` +```@docs +TwoLevelTree +regular_tree +``` + # Properties of time periods ```@docs duration ``` - ```@docs isfirst ``` @@ -43,7 +47,6 @@ isfirst multiple ``` - ```@docs probability ``` @@ -60,6 +63,11 @@ opscenarios ```@docs strat_periods +strategic_periods +``` + +```@docs +strategic_scenarios ``` ```@docs @@ -97,6 +105,10 @@ ScenarioProfile StrategicProfile ``` +```@docs +StrategicStochasticProfile +``` + # Discounting ```@docs diff --git a/docs/src/reference/internal.md b/docs/src/reference/internal.md new file mode 100644 index 0000000..310adc0 --- /dev/null +++ b/docs/src/reference/internal.md @@ -0,0 +1,82 @@ +# Internal types + +```@meta +CurrentModule = TimeStruct +``` + +## Strategic period types ([`TwoLevelTree`](@ref)) + +### Single types + +```@docs +AbstractTreeNode +StratNode +StrategicScenario +``` + +### Iterator types + +```@docs +AbstractTreeStructure +StratTreeNodes + +``` + +## Strategic period types ([`TwoLevel`](@ref)) + +### Single types + +```@docs +AbstractStrategicPeriod +StrategicPeriod +``` + +### Iterator types + +```@docs +``` + +## Representative period types + +### Single types + +```@docs +AbstractRepresentativePeriod +RepresentativePeriod +StratNodeReprPeriod +``` + +### Iterator types + +```@docs +StratNodeReprPeriods +``` + +## Operational scenarios types + +### Single types + +```@docs +OperationalScenario +StratOperationalScenario +StratNodeOperationalScenario +StratNodeReprOpscenario +``` + +### Iterator types + +```@docs +StrategicScenarios +StratNodeOpScens +StratNodeReprOpscenarios +``` + +## Operational period types + +```@docs +TimePeriod +SimplePeriod +CalendarPeriod +OperationalPeriod +TreePeriod +``` diff --git a/src/TimeStruct.jl b/src/TimeStruct.jl index a9d68a9..d1f327e 100644 --- a/src/TimeStruct.jl +++ b/src/TimeStruct.jl @@ -12,14 +12,17 @@ include("calendar.jl") include("representative.jl") include("stochastic.jl") include("twolevel.jl") -include("twoleveltree.jl") include("strat_periods.jl") include("repr_periods.jl") include("opscenarios.jl") -include("profiles.jl") include("utils.jl") include("discount.jl") +include("tree_nodes.jl") +include("twoleveltree.jl") + +include("profiles.jl") + export TimeStructure export SimpleTimes export CalendarTimes @@ -34,13 +37,12 @@ export OperationalProfile export ScenarioProfile export StrategicProfile export StrategicStochasticProfile -export DynamicStochasticProfile export RepresentativeProfile export opscenarios export repr_periods export strat_periods, strategic_periods -export regular_tree, strat_nodes, scenarios +export regular_tree, strat_nodes, strategic_scenarios export withprev, chunk, chunk_duration export isfirst, duration, duration_strat, multiple, probability export multiple_strat diff --git a/src/profiles.jl b/src/profiles.jl index b85a56a..f5ae669 100644 --- a/src/profiles.jl +++ b/src/profiles.jl @@ -165,18 +165,63 @@ function Base.getindex( return _value_lookup(RepresentativeIndexable(T), rp, period) end -struct StrategicStochasticProfile{T} <: TimeProfile{T} - vals::Vector{Vector{T}} -end -function Base.getindex(ssp::StrategicStochasticProfile, i::TimePeriod) - return ssp.vals[_strat_per(i)][_branch(i)] -end +""" + StrategicStochasticProfile(vals::Vector{<:Vector{P}}) where {T<:Number, P<:TimeProfile{T}} + StrategicStochasticProfile(vals::Vector{<:Vector{<:Number}}) + +Time profile with a separate time profile for each strategic node in a [`TwoLevelTree`](@ref) +structure. -struct DynamicStochasticProfile{T<:Number} <: TimeProfile{T} - vals::Vector{<:Vector{<:TimeProfile{T}}} +If too few profiles are provided, the last given profile will be repeated, both for strategic +periods and branches within a strategic period. + +## Example +```julia + # The same value in each strategic period and branch +profile = StrategicStochasticProfile([[1], [21, 22]]) +# Varying values in each strategic period and branch +profile = StrategicStochasticProfile([ + [OperationalProfile([11, 12])], + [OperationalProfile([21, 22]), OperationalProfile([31, 32])] +]) +``` +""" +struct StrategicStochasticProfile{T<:Number,P<:TimeProfile{T}} <: TimeProfile{T} + vals::Vector{<:Vector{P}} +end +function StrategicStochasticProfile(vals::Vector{<:Vector{<:Number}}) + return StrategicStochasticProfile([ + [FixedProfile(v_2) for v_2 in v_1] for v_1 in vals + ]) +end + +function _value_lookup( + ::HasStratTreeIndex, + ssp::StrategicStochasticProfile, + period, +) + sp_prof = ssp.vals[_strat_per(period) > length(ssp.vals) ? end : + _strat_per(period)] + branch_prof = + sp_prof[_branch(period) > length(sp_prof) ? end : _branch(period)] + return branch_prof[period] +end + +function _value_lookup( + ::NoStratTreeIndex, + ssp::StrategicStochasticProfile, + period, +) + return error( + "Type $(typeof(period)) can not be used as index for a strategic stochastic profile", + ) end -function Base.getindex(ssp::DynamicStochasticProfile, i::TimePeriod) - return ssp.vals[_strat_per(i)][_branch(i)][i] + +function Base.getindex( + ssp::StrategicStochasticProfile, + period::T, +) where {T<:Union{TimePeriod,TimeStructure}} + return _value_lookup(StrategicTreeIndexable(T), ssp, period) end Base.getindex(TP::TimeProfile, inds...) = [TP[i] for i in inds] diff --git a/src/strat_periods.jl b/src/strat_periods.jl index 056e2ac..8aa6b6a 100644 --- a/src/strat_periods.jl +++ b/src/strat_periods.jl @@ -205,4 +205,10 @@ _strat_per(ssp::SingleStrategicPeriod) = 1 strat_periods(ts::TimeStructure) = SingleStrategicPeriodWrapper(ts) # Allow strategic_periods() in addition to strat_periods() + +""" + strategic_periods(ts) + +Internal convenience constructor for [`strat_periods`](@ref). +""" strategic_periods(ts) = strat_periods(ts) diff --git a/src/tree_nodes.jl b/src/tree_nodes.jl new file mode 100644 index 0000000..e4a07e1 --- /dev/null +++ b/src/tree_nodes.jl @@ -0,0 +1,398 @@ +""" + AbstractTreeNode{S,T} <: AbstractStrategicPeriod{S,T} + +Abstract base type for all tree nodes within a [`TwoLevelTree`] type. +""" +abstract type AbstractTreeNode{S,T} <: AbstractStrategicPeriod{S,T} end + +""" + AbstractTreeStructure + +Abstract base type for all tree timestructures within a [`TwoLevelTree`] type. +""" +abstract type AbstractTreeStructure end + +Base.length(ats::AbstractTreeStructure) = length(_oper_struct(ats)) +function Base.iterate(ats::AbstractTreeStructure, state = (nothing, 1)) + next = + isnothing(state[1]) ? iterate(_oper_struct(ats)) : + iterate(_oper_struct(ats), state[1]) + isnothing(next) && return nothing + + return strat_node_period(ats, next[1], state[2]), (next[2], state[2] + 1) +end + +abstract type StrategicTreeIndexable end +struct HasStratTreeIndex <: StrategicTreeIndexable end +struct NoStratTreeIndex <: StrategicTreeIndexable end + +StrategicTreeIndexable(::Type) = NoStratTreeIndex() +StrategicTreeIndexable(::Type{<:AbstractTreeNode}) = HasStratTreeIndex() +StrategicTreeIndexable(::Type{<:TimePeriod}) = HasStratTreeIndex() + +""" + struct StratNode{S, T, OP<:TimeStructure{T}} <: AbstractTreeNode{S,T} + +A structure representing a single strategic node of a [`TwolevelTree`]. It is created +through iterating through [`StratTreeNodes`](@ref). + +It is equivalent to a [`StrategicPeriod`](@ref) of a [`TwoLevel`](@ref) time structure when +utilizing a [`TwolevelTree`]. +""" +struct StratNode{S,T,OP<:TimeStructure{T}} <: AbstractTreeNode{S,T} + sp::Int + branch::Int + duration::S + prob_branch::Float64 + mult_sp::Float64 + parent::Any + operational::OP +end + +_strat_per(n::StratNode) = n.sp +_branch(n::StratNode) = n.branch + +probability_branch(n::StratNode) = n.prob_branch +duration_strat(n::StratNode) = n.duration +multiple_strat(sp::StratNode, t) = multiple(t) / duration_strat(sp) + +isfirst(n::StratNode) = n.sp == 1 + +# Adding methods to existing Julia functions +Base.show(io::IO, n::StratNode) = print(io, "sp$(n.sp)-br$(n.branch)") +Base.length(n::StratNode) = length(n.operational) +Base.eltype(::Type{StratNode}) = TreePeriod +function Base.iterate(n::StratNode, state = nothing) + next = + isnothing(state) ? iterate(n.operational) : + iterate(n.operational, state) + next === nothing && return nothing + + return TreePeriod(n, next[1]), next[2] +end + +""" + struct StratNodeOperationalScenario{T,OP<:TimeStructure{T}} <: AbstractOperationalScenario{T} + +A structure representing a single operational scenario for a strategic node supporting +iteration over its time periods. It is created through iterating through +[`StratNodeOpScens`](@ref). + +It is equivalent to a [`StratOperationalScenario`](@ref) of a [`TwoLevel`](@ref) time +structure when utilizing a [`TwolevelTree`]. +""" +struct StratNodeOperationalScenario{T,OP<:TimeStructure{T}} <: + AbstractOperationalScenario{T} + sp::Int + branch::Int + scen::Int + mult_sp::Float64 + mult_scen::Float64 + prob_branch::Float64 + prob_scen::Float64 + operational::OP +end + +_opscen(osc::StratNodeOperationalScenario) = osc.scen +_branch(osc::StratNodeOperationalScenario) = osc.branch +_strat_per(osc::StratNodeOperationalScenario) = osc.sp + +probability(osc::StratNodeOperationalScenario) = osc.prob_branch * prob_scen +probability_branch(osc::StratNodeOperationalScenario) = osc.prob_branch +mult_scen(osc::StratNodeOperationalScenario) = osc.mult_scen + +function StrategicTreeIndexable(::Type{<:StratNodeOperationalScenario}) + return HasStratTreeIndex() +end +StrategicIndexable(::Type{<:StratNodeOperationalScenario}) = HasStratIndex() + +# Adding methods to existing Julia functions +function Base.show(io::IO, osc::StratNodeOperationalScenario) + return print(io, "sp$(osc.sp)-br$(osc.branch)-sc$(osc.scen)") +end +Base.eltype(_::StratNodeOperationalScenario) = TreePeriod + +""" + struct StratNodeOpScens <: AbstractTreeStructure + +Type for iterating through the individual operational scenarios of a [`StratNode`](@ref). +It is automatically created through the function [`opscenarios`](@ref). +""" +struct StratNodeOpScens <: AbstractTreeStructure + sp::Int + branch::Int + mult_sp::Float64 + prob_branch::Float64 + opscens::Any +end + +""" + opscenarios(sp::StratNode{S,T,OP}) + +When the `TimeStructure` is a [`StratNode`](@ref), `opscenarios` returns a +[`StratNodeOpScens`](@ref) used for iterating through the individual operational scenarios +""" +function opscenarios(n::StratNode{S,T,OP}) where {S,T,OP<:TimeStructure{T}} + return StratNodeOpScens( + _strat_per(n), + _branch(n), + n.mult_sp, + probability_branch(n), + opscenarios(n.operational), + ) +end +function opscenarios(n::StratNode{S,T,OP}) where {S,T,OP<:RepresentativePeriods} + return collect(Iterators.flatten(opscenarios(rp) for rp in repr_periods(n))) +end + +_oper_struct(oscs::StratNodeOpScens) = oscs.opscens +function strat_node_period(oscs::StratNodeOpScens, next, state) + return StratNodeOperationalScenario( + oscs.sp, + oscs.branch, + state, + oscs.mult_sp, + mult_scen(next), + oscs.prob_branch, + probability(next), + next, + ) +end + +Base.eltype(_::StratNodeOpScens) = StratNodeOperationalScenario + +""" + struct StratNodeReprPeriod{T,OP<:TimeStructure{T}} <: AbstractRepresentativePeriod{T} + +A structure representing a single representative period of a [`StratNode`](@ref) of a +[`TwolevelTree`]. It is created through iterating through [`StratNodeReprPeriods`](@ref). + +It is equivalent to a [`StratReprPeriod`] of a [`TwoLevel`](@ref) time structure when +utilizing a [`TwolevelTree`]. +""" +struct StratNodeReprPeriod{T,OP<:TimeStructure{T}} <: + AbstractRepresentativePeriod{T} + sp::Int + branch::Int + rp::Int + mult_sp::Float64 + mult_rp::Float64 + prob_branch::Float64 + operational::OP +end + +_rper(rp::StratNodeReprPeriod) = rp.rp +_branch(rp::StratNodeReprPeriod) = rp.branch +_strat_per(rp::StratNodeReprPeriod) = rp.sp + +probability(rp::StratNodeReprPeriod) = rp.prob_branch +probability_branch(rp::StratNodeReprPeriod) = rp.prob_branch +function multiple(rp::StratNodeReprPeriod, t::OperationalPeriod) + return t.multiple / rp.mult_sp +end + +StrategicTreeIndexable(::Type{<:StratNodeReprPeriod}) = HasStratTreeIndex() +StrategicIndexable(::Type{<:StratNodeReprPeriod}) = HasStratIndex() + +# Adding methods to existing Julia functions +function Base.show(io::IO, rp::StratNodeReprPeriod) + return print(io, "sp$(rp.sp)-br$(rp.branch)-rp$(rp.rp)") +end +Base.eltype(_::StratNodeReprPeriod) = TreePeriod + +""" + struct StratNodeReprPeriods <: AbstractTreeStructure + +Type for iterating through the individual presentative periods of a [`StratNode`](@ref). +It is automatically created through the function [`repr_periods`](@ref). +""" +struct StratNodeReprPeriods <: AbstractTreeStructure + sp::Int + branch::Int + mult_sp::Float64 + prob_branch::Float64 + repr::Any +end + +""" + repr_periods(sp::StratNode{S,T,OP}) + +Iterator that iterates over operational scenarios for a specific strategic node in the tree. +""" +function repr_periods(n::StratNode{S,T,OP}) where {S,T,OP<:TimeStructure{T}} + return StratNodeReprPeriods( + _strat_per(n), + _branch(n), + n.mult_sp, + probability_branch(n), + repr_periods(n.operational), + ) +end + +_oper_struct(rps::StratNodeReprPeriods) = rps.repr +function strat_node_period(rps::StratNodeReprPeriods, next, state) + return StratNodeReprPeriod( + rps.sp, + rps.branch, + state, + rps.mult_sp, + mult_repr(next), + rps.prob_branch, + next, + ) +end + +Base.eltype(_::StratNodeReprPeriods) = StratNodeReprPeriod + +""" + struct StratNodeReprOpscenario{T} <: AbstractOperationalScenario{T} + +A structure representing a single operational scenario for a representative period in A +[`TwoLevelTree`] structure supporting iteration over its time periods. +""" +struct StratNodeReprOpscenario{T,OP<:TimeStructure{T}} <: + AbstractOperationalScenario{T} + sp::Int + branch::Int + rp::Int + opscen::Int + mult_sp::Float64 + mult_rp::Float64 + prob_branch::Float64 + prob_scen::Float64 + operational::OP +end + +_opscen(osc::StratNodeReprOpscenario) = osc.opscen +_rper(osc::StratNodeReprOpscenario) = osc.rp +_branch(osc::StratNodeReprOpscenario) = osc.branch +_strat_per(osc::StratNodeReprOpscenario) = osc.sp + +probability(osc::StratNodeReprOpscenario) = osc.prob_branch * osc.prob_scen +probability_branch(osc::StratNodeReprOpscenario) = osc.prob_branch + +StrategicTreeIndexable(::Type{<:StratNodeReprOpscenario}) = HasStratTreeIndex() +StrategicIndexable(::Type{<:StratNodeReprOpscenario}) = HasStratIndex() +function RepresentativeIndexable(::Type{<:StratNodeReprOpscenario}) + return HasReprIndex() +end +ScenarioIndexable(::Type{<:StratNodeReprOpscenario}) = HasScenarioIndex() + +# Adding methods to existing Julia functions +function Base.show(io::IO, osc::StratNodeReprOpscenario) + return print(io, "sp$(osc.sp)-br$(osc.branch)-rp$(osc.rp)-sc$(osc.opscen)") +end +Base.eltype(_::StratNodeReprOpscenario) = TreePeriod + +""" + struct StratNodeReprOpscenarios <: AbstractTreeStructure + +Type for iterating through the individual operational scenarios of a +[`StratNodeReprPeriod`](@ref). It is automatically created through the function +[`opscenarios`](@ref). +""" +struct StratNodeReprOpscenarios <: AbstractTreeStructure + sp::Int + branch::Int + rp::Int + mult_sp::Float64 + mult_rp::Float64 + prob_branch::Float64 + opscens::Any +end + +function StratNodeReprOpscenarios( + rp::StratNodeReprPeriod{T,OP}, + opscens, +) where {T,OP<:TimeStructure{T}} + return StratNodeReprOpscenarios( + _strat_per(rp), + _branch(rp), + _rper(rp), + rp.mult_sp, + rp.mult_rp, + probability_branch(rp), + opscens, + ) +end + +""" + opscenarios(sp::StratNodeReprPeriod{T,RepresentativePeriod{T,OP}}) + +Iterator that iterates over operational scenarios for a specific representative period of +a strategic node in the tree. +""" +function opscenarios( + rp::StratNodeReprPeriod{T,RepresentativePeriod{T,OP}}, +) where {T,OP} + if _strat_per(rp) == 1 && _rper(rp) == 1 + end + return StratNodeReprOpscenarios( + _strat_per(rp), + _branch(rp), + _rper(rp), + rp.mult_sp, + rp.mult_rp, + probability_branch(rp), + opscenarios(rp.operational.operational), + ) +end +function opscenarios( + rp::StratNodeReprPeriod{T,SingleReprPeriod{T,OP}}, +) where {T,OP} + if _strat_per(rp) == 1 && _rper(rp) == 1 + end + return StratNodeOpScens( + _strat_per(rp), + _branch(rp), + rp.mult_sp, + probability_branch(rp), + opscenarios(rp.operational.ts), + ) +end + +_oper_struct(oscs::StratNodeReprOpscenarios) = oscs.opscens +function strat_node_period(oscs::StratNodeReprOpscenarios, next, state) + return StratNodeReprOpscenario( + oscs.sp, + oscs.branch, + oscs.rp, + state, + oscs.mult_sp, + oscs.mult_rp, + oscs.prob_branch, + probability(next), + next, + ) +end + +Base.eltype(_::StratNodeReprOpscenarios) = StratNodeReprOpscenario + +# All introduced subtypes require the same procedures for the iteration and indexing. +# Hence, all introduced types use the same functions. +TreeStructure = Union{ + StratNodeOperationalScenario, + StratNodeReprPeriod, + StratNodeReprOpscenario, +} +Base.length(ts::TreeStructure) = length(ts.operational) +function Base.last(ts::TreeStructure) + per = last(ts.operational) + return TreePeriod(ts, per) +end + +function Base.getindex(ts::TreeStructure, index) + per = ts.operational[index] + return TreePeriod(ts, per) +end +function Base.eachindex(ts::TreeStructure) + return eachindex(ts.operational) +end +function Base.iterate(ts::TreeStructure, state = nothing) + next = + isnothing(state) ? iterate(ts.operational) : + iterate(ts.operational, state) + isnothing(next) && return nothing + + return TreePeriod(ts, next[1]), next[2] +end diff --git a/src/twoleveltree.jl b/src/twoleveltree.jl index 83758a8..61c8b6a 100644 --- a/src/twoleveltree.jl +++ b/src/twoleveltree.jl @@ -1,274 +1,242 @@ -abstract type AbstractTreeNode{T} end - """ mutable struct TwoLevelTree{T} <: TimeStructure{T} - Time structure allowing for a tree structure for - the strategic level. +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. +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. """ -mutable struct TwoLevelTree{T} <: TimeStructure{T} +mutable struct TwoLevelTree{T,OP<:AbstractTreeNode{T}} <: TimeStructure{T} len::Int root::Any - nodes::Vector{<:AbstractTreeNode{T}} + nodes::Vector{OP} + op_per_strat::Float64 +end + +function TwoLevelTree{T,OP}( + nodes::Vector{OP}, + op_per_strat, +) where {T,OP<:AbstractTreeNode{T}} + return TwoLevelTree{T,OP}(0, nothing, nodes, op_per_strat) end -function TwoLevelTree{T}(nodes::Vector{<:AbstractTreeNode}) where {T} - return TwoLevelTree{T}(0, nothing, nodes) +function Base.length(itr::TwoLevelTree) + return sum(length(n.operational) for n in itr.nodes) end +Base.eltype(::Type{TwoLevelTree{T,OP}}) where {T,OP} = TreePeriod + +function _multiple_adj(itr::TwoLevelTree, n) + mult = + itr.nodes[n].duration * itr.op_per_strat / + _total_duration(itr.nodes[n].operational) + return stripunit(mult) +end +strat_nodes(tree::TwoLevelTree) = tree.nodes + +function children(n::StratNode, ts::TwoLevelTree) + return [c for c in ts.nodes if c.parent == n] +end +nchildren(n::StratNode, ts::TwoLevelTree) = count(c -> c.parent == n, ts.nodes) + +branches(tree::TwoLevelTree, sp) = count(n -> n.sp == sp, tree.nodes) + +leaves(ts::TwoLevelTree) = [n for n in ts.nodes if nchildren(n, ts) == 0] +nleaves(ts::TwoLevelTree) = count(n -> nchildren(n, ts) == 0, ts.nodes) +getleaf(ts::TwoLevelTree, leaf) = leaves(ts)[leaf] """ - struct OperPeriod <: TimePeriod + struct TreePeriod{P} <: TimePeriod where {P<:TimePeriod} - Time period for iteration of a TwoLevelTree time structure. +Time period for iteration of a `TwoLevelTree` time structure. This period has in addition +to an operational period also the two fields `branch` and `prob_branch` corresponding to the +respective branch and probability of the branch + +!!! warn "Using OperationalScenarios" + The probability will always only correspond to the branch probability, even when you + utilize `OperationalScenarios`. Using the function `probability` includes however the + scenario probability. """ -struct OperPeriod <: TimePeriod +struct TreePeriod{P} <: TimePeriod where {P<:TimePeriod} sp::Int branch::Int - sc::Int - op::Int - duration::Float64 - prob::Float64 + prob_branch::Float64 + multiple::Float64 + period::P end -duration(t::OperPeriod) = t.duration -probability(t::OperPeriod) = t.prob -_oper(t::OperPeriod) = t.op -_opscen(t::OperPeriod) = t.sc -_strat_per(t::OperPeriod) = t.sp -_branch(t::OperPeriod) = t.branch +isfirst(t::TreePeriod) = isfirst(t.period) +duration(t::TreePeriod) = duration(t.period) +probability_branch(t::TreePeriod) = t.prob_branch +probability(t::TreePeriod) = probability(t.period) * probability_branch(t) +multiple(t::TreePeriod) = t.multiple -function Base.length(itr::TwoLevelTree) - return sum(length(n.strat_node.operational) for n in itr.nodes) -end -Base.eltype(::Type{TwoLevelTree{T}}) where {T} = OperPeriod +_oper(t::TreePeriod) = _oper(t.period) +_opscen(t::TreePeriod) = _opscen(t.period) +_rper(t::TreePeriod) = _rper(t.period) +_branch(t::TreePeriod) = t.branch +_strat_per(t::TreePeriod) = t.sp -# Iterate through all time periods as OperationalPeriods -function Base.iterate(itr::TwoLevelTree) - spn = itr.nodes[1].strat_node - next = iterate(spn.operational) - next === nothing && return nothing - per = next[1] - return OperPeriod( - spn.sp, - spn.branch, - _opscen(per), - _oper(per), - duration(per), - probability(spn) * probability(per), - ), - (1, next[2]) +function Base.show(io::IO, t::TreePeriod) + return print(io, "sp$(t.sp)-br$(t.branch)-$(t.period)") +end +function Base.isless(t1::TreePeriod, t2::TreePeriod) + return t1.period < t2.period end -function Base.iterate(itr::TwoLevelTree, state) +function Base.iterate(itr::TwoLevelTree, state = (1, nothing)) i = state[1] - spn = itr.nodes[i].strat_node - next = iterate(spn.operational, state[2]) + spn = itr.nodes[i] + next = + isnothing(state[2]) ? iterate(spn.operational) : + iterate(spn.operational, state[2]) if next === nothing i = i + 1 if i > length(itr.nodes) return nothing end - spn = itr.nodes[i].strat_node + spn = itr.nodes[i] next = iterate(spn.operational) end per = next[1] - return OperPeriod( - spn.sp, - spn.branch, - _opscen(per), - _oper(per), - duration(per), - probability(spn) * probability(per), - ), + + mult = _multiple_adj(itr, i) * multiple(per) + return TreePeriod(spn.sp, spn.branch, probability_branch(spn), mult, per), (i, next[2]) end -struct StratNode{T} <: TimePeriod - sp::Int - branch::Int - duration::Duration - probability::Float64 - operational::TimeStructure{T} - time_struct::TimeStructure +# Convenient constructors for the individual types +function TreePeriod( + n::StratNode, + per::P, +) where {P<:Union{TimePeriod,TimeStructure}} + mult = n.mult_sp * multiple(per) + return TreePeriod(n.sp, n.branch, n.prob_branch, mult, per) end - -Base.show(io::IO, n::StratNode) = print(io, "sp$(n.sp)-br$(n.branch)") -_branch(n::StratNode) = n.branch -_strat_per(n::StratNode) = n.sp -probability(n::StratNode) = n.probability -duration(n::StratNode) = n.duration - -isfirst(n::StratNode) = n.sp == 1 - -struct TreeNode{T} <: AbstractTreeNode{T} - node::Int - parent::Union{Nothing,TreeNode} - strat_node::StratNode{T} +function TreePeriod( + osc::StratNodeOperationalScenario, + per::P, +) where {P<:Union{TimePeriod,AbstractOperationalScenario}} + mult = osc.mult_sp * multiple(per) + return TreePeriod(osc.sp, osc.branch, osc.prob_branch, mult, per) end - -children(n::TreeNode, ts::TwoLevelTree) = [c for c in ts.nodes if c.parent == n] -nchildren(n::TreeNode, ts::TwoLevelTree) = count(c -> c.parent == n, ts.nodes) -strat_nodes(ts::TwoLevelTree) = [n.strat_node for n in ts.nodes] -strat_periods(ts::TwoLevelTree) = strat_nodes(ts) - -# Iterate through time periods of a strategic node -Base.length(n::StratNode) = length(n.operational) -Base.eltype(::Type{StratNode{T}}) where {T} = OperPeriod -function Base.iterate(itr::StratNode, state = nothing) - next = - isnothing(state) ? iterate(itr.operational) : - iterate(itr.operational, state) - next === nothing && return nothing - per = next[1] - return OperPeriod( - itr.sp, - itr.branch, - _opscen(per), - _oper(per), - duration(per), - itr.probability * probability(per), - ), - next[2] +function TreePeriod( + rp::StratNodeReprPeriod, + per::P, +) where {P<:Union{TimePeriod,AbstractRepresentativePeriod}} + mult = rp.mult_sp * multiple(per) + return TreePeriod(rp.sp, rp.branch, rp.prob_branch, mult, per) end - -struct StratNodeOperationalScenario{T} <: TimeStructure{T} - node::StratNode - scen::Int - probability::Float64 - operational::TimeStructure{T} +function TreePeriod( + osc::StratNodeReprOpscenario, + per::P, +) where {P<:Union{TimePeriod,AbstractOperationalScenario}} + rper = ReprPeriod(osc.rp, per, osc.mult_rp * multiple(per)) + mult = osc.mult_sp * osc.mult_rp * multiple(per) + return TreePeriod(osc.sp, osc.branch, osc.prob_branch, mult, rper) end -# Iteration through all time periods in an operational scenario of a strategic node -Base.length(snops::StratNodeOperationalScenario) = length(snops.operational) -Base.eltype(_::StratNodeOperationalScenario) = OperPeriod -function Base.iterate(snops::StratNodeOperationalScenario, state = nothing) - next = - isnothing(state) ? iterate(snops.operational) : - iterate(snops.operational, state) - isnothing(next) && return nothing - per = next[1] - return OperPeriod( - snops.node.sp, - snops.node.branch, - snops.scen, - _oper(per), - duration(per), - snops.probability, - ), - next[2] -end +""" + struct StrategicScenario -# Iterate through operational scenarios of a strategic node -struct StratNodeOpScens{TS} - node::StratNode - operational::TS +Desription 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. +""" +struct StrategicScenario + probability::Float64 + nodes::Vector{<:StratNode} end -opscenarios(n::StratNode) = StratNodeOpScens(n, n.operational) - -function Base.length( - sops::StratNodeOpScens{OperationalScenarios{T,OP}}, -) where {T,OP} - return sops.operational.len -end -function Base.iterate( - sops::StratNodeOpScens{OperationalScenarios{T,OP}}, - state = 1, -) where {T,OP} - state > sops.operational.len && return nothing - return StratNodeOperationalScenario( - sops.node, - state, - sops.node.probability * sops.operational.probability[state], - sops.operational.scenarios[state], - ), - state + 1 -end +# Iterate through strategic periods of scenario +Base.length(scen::StrategicScenario) = length(scen.nodes) +Base.last(scen::StrategicScenario) = last(scen.nodes) -function Base.length(sops::StratNodeOpScens{SimpleTimes{T}}) where {T} - return 1 -end -function Base.iterate( - sops::StratNodeOpScens{SimpleTimes{T}}, - state = 1, -) where {T} - state > 1 && return nothing - return StratNodeOperationalScenario( - sops.node, - state, - sops.node.probability, - sops.operational, - ), - state + 1 +function Base.iterate(scs::StrategicScenario, state = nothing) + next = isnothing(state) ? iterate(scs.nodes) : iterate(scs.nodes, state) + isnothing(next) && return nothing + return next[1], next[2] end -struct Scenario - probability::Float64 - nodes::Vector{TreeNode} -end +""" + struct StrategicScenarios -# Iterate through all scenarios -struct Scenarios +Type for iteration through the individual strategic scenarios represented as +[`StrategicScenario`]. +""" +struct StrategicScenarios ts::TwoLevelTree end -scenarios(ts::TwoLevelTree) = Scenarios(ts) +""" + strategic_scenarios(ts::TwoLevelTree) -nleaves(ts::TwoLevelTree) = count(n -> nchildren(n, ts) == 0, ts.nodes) -leaves(ts::TwoLevelTree) = [n for n in ts.nodes if nchildren(n, ts) == 0] -getleaf(ts::TwoLevelTree, leaf) = leaves(ts)[leaf] +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 +""" +strategic_scenarios(two_level::TwoLevel) = [two_level] -Base.length(scens::Scenarios) = nleaves(scens.ts) -function Base.iterate(scs::Scenarios, state = 1) +""" +When the `TimeStructure` is a [`TwoLevelTree`](@ref), `strategic_scenarios` returns the +iterator `StrategicScenarios`. +""" +strategic_scenarios(ts::TwoLevelTree) = StrategicScenarios(ts) +# Allow a TwoLevel structure to be used as a tree with one scenario +# TODO: Should be replaced with a single wrapper as it is the case for the other scenarios + +Base.length(scens::StrategicScenarios) = nleaves(scens.ts) +function Base.iterate(scs::StrategicScenarios, state = 1) if state > nleaves(scs.ts) return nothing end node = getleaf(scs.ts, state) - prob = probability(node.strat_node) + prob = probability_branch(node) nodes = [node] while !isnothing(node.parent) node = node.parent pushfirst!(nodes, node) end - return Scenario(prob, nodes), state + 1 -end - -# Iterate through strategic periods of scenario -Base.length(scen::Scenario) = length(scen.nodes) -function Base.iterate(scs::Scenario, state = nothing) - next = isnothing(state) ? iterate(scs.nodes) : iterate(scs.nodes, state) - isnothing(next) && return nothing - return next[1].strat_node, next[2] + return StrategicScenario(prob, nodes), state + 1 end -branches(tree::TwoLevelTree, sp) = count(n -> n.strat_node.sp == sp, tree.nodes) - -# Allow a TwoLevel structure to be used as a tree with one scenario -scenarios(two_level::TwoLevel{S,T}) where {S,T} = [two_level] - +""" + 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 nodes iteratively in a depth first manner function add_node( - tree::TwoLevelTree{T}, + tree::TwoLevelTree{T,StratNode{S,T,OP}}, parent, - index, sp, - duration, + duration::S, branch_prob, branching, - ts::TimeStructure{T}, -) where {T} - prob = - branch_prob * (isnothing(parent) ? 1.0 : parent.strat_node.probability) - node = TreeNode{T}( - index, + 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}( + sp, + branches(tree, sp) + 1, + duration, + prob_branch, + mult_sp, parent, - StratNode{T}(sp, branches(tree, sp) + 1, duration, prob, ts, tree), + oper, ) push!(tree.nodes, node) if isnothing(parent) @@ -281,26 +249,117 @@ function add_node( add_node( tree, node, - length(tree.nodes) + 1, sp + 1, duration, 1.0 / branching[sp], branching, - ts, + oper, ) end end end -# Create a regular tree with the given branching structure and the same time structure in each node +""" + regular_tree( + duration::S, + branching::Vector, + ts::OP; + op_per_strat::Real=1.0, + ) where {S, T, OP<:TimeStructure{T}} + +Function for creating a regular tree. +""" function regular_tree( - duration, + duration::S, branching::Vector, - ts::TimeStructure{T}, -) where {T} - tree = TwoLevelTree{T}(Vector{TreeNode{T}}()) + ts::OP; + op_per_strat::Real = 1.0, +) where {S,T,OP<:TimeStructure{T}} + tree = TwoLevelTree{T,StratNode{S,T,OP}}( + Vector{StratNode{S,T,OP}}(), + op_per_strat, + ) tree.len = length(branching) + 1 - add_node(tree, nothing, 1, 1, duration, 1.0, branching, ts) + add_node(tree, nothing, 1, duration, 1.0, branching, ts) return tree end + +""" + struct StratTreeNodes{T, OP} <: AbstractTreeStructure + +Type for iterating through the individual strategic nodes of a [`TwoLevelTree`](@ref). +It is automatically created through the function [`strat_periods`](@ref), and hence, +[`strategic_periods`](@ref). + +Iterating through `StratTreeNodes` using the `WithPrev` iterator changes the behaviour, +although the meaining remains unchanged. +""" +struct StratTreeNodes{T,OP} <: AbstractTreeStructure + ts::TwoLevelTree{T,OP} +end + +# Adding methods to existing Julia functions +Base.length(sps::StratTreeNodes) = length(sps.ts.nodes) +Base.eltype(_::Type{StratTreeNodes{T,OP}}) where {T,OP} = OP +function Base.iterate(stps::StratTreeNodes, state = nothing) + next = isnothing(state) ? 1 : state + 1 + next == length(stps) + 1 && return nothing + return stps.ts.nodes[next], next +end + +function Base.iterate(w::WithPrev{StratTreeNodes{T,OP}}) where {T,OP} + n = iterate(w.itr) + n === nothing && return n + return (nothing, n[1]), (n[1], n[2]) +end + +function Base.iterate(w::WithPrev{StratTreeNodes{T,OP}}, state) where {T,OP} + n = iterate(w.itr, state[2]) + n === nothing && return n + return (n[1].parent, n[1]), (n[1], n[2]) +end + +"""When the `TimeStructure` is a [`TwoLevelTree`](@ref), `strat_periods` returns a +[`StratTreeNodes`](@ref) type, which, through iteration, provides [`StratNode`](@ref) types. + +These are equivalent to a [`StrategicPeriod`](@ref) of a [`TwoLevel`](@ref) time structure. +""" +strat_periods(ts::TwoLevelTree) = StratTreeNodes(ts) + +""" +When the `TimeStructure` is a [`TwoLevelTree`](@ref), `opscenarios` returns an `Array` of +all [`StratNodeOperationalScenario`](@ref)s or [`StratNodeReprOpscenario`](@ref)s types, +dependening on whether the [`TwoLevelTree`](@ref) includes [`RepresentativePeriods`](@ref) +or not. + +These are equivalent to a [`StratOperationalScenario`](@ref) of a [`TwoLevel`](@ref) time +structure. +""" +function opscenarios(ts::TwoLevelTree) + return collect( + Iterators.flatten(opscenarios(sp) for sp in strat_periods(ts)), + ) +end +function opscenarios( + ts::TwoLevelTree{T,StratNode{S,T,OP}}, +) where {S,T,OP<:RepresentativePeriods} + return collect( + Iterators.flatten( + opscenarios(rp) for sp in strat_periods(ts) for + rp in repr_periods(sp) + ), + ) +end + +""" +When the `TimeStructure` is a [`TwoLevelTree`](@ref), `repr_periods` returns an `Array` of +all [`StratNodeReprPeriod`]s. + +These are equivalent to a [`StratReprPeriod`] of a [`TwoLevel`](@ref) time structure. +""" +function repr_periods(ts::TwoLevelTree) + return collect( + Iterators.flatten(repr_periods(sp) for sp in strat_periods(ts)), + ) +end diff --git a/test/Project.toml b/test/Project.toml index 956df01..1ce0b8b 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -5,9 +5,10 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" LocalRegistry = "89398ba2-070a-4b16-a995-9893c55d93cf" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" +TestItems = "1c621080-faea-4a02-84b6-bbd5e436b8fe" TimeStruct = "f9ed5ce0-9f41-4eaa-96da-f38ab8df101c" TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" [compat] -Aqua = "0.7" \ No newline at end of file +Aqua = "0.7" diff --git a/test/runtests.jl b/test/runtests.jl index bc3befa..ec06e4d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,3 +1,4 @@ +using TestItems using TestItemRunner using TimeStruct using Aqua @@ -878,12 +879,89 @@ end @test sum(sspdiv[t] for t in tsc) == 200 / 0.5 end -@testitem "TwoLevelTree" begin +# Function for testing all iterators. Can be beneficial for all other tests as well +@testmodule TwoLevelTreeTest begin + using TimeStruct, Test + function fun(ts, n_sp, n_op; n_sc = nothing, n_rp = nothing) + if isnothing(n_rp) + n_rp = n_sp + end + if isnothing(n_sc) + n_sc = n_rp + end + + # Test the invariants for operational periods + sps = strat_periods(ts) + ops_inv = Array{Any}(nothing, 8) + ops_inv[1] = collect(ts) + ops_inv[2] = [t for sp in sps for t in sp] + + ops_inv[3] = [t for rp in repr_periods(ts) for t in rp] + ops_inv[4] = [t for sp in sps for rp in repr_periods(sp) for t in rp] + + ops_inv[5] = [t for sc in opscenarios(ts) for t in sc] + ops_inv[6] = [t for sp in sps for sc in opscenarios(sp) for t in sc] + ops_inv[7] = + [t for rp in repr_periods(ts) for sc in opscenarios(rp) for t in sc] + ops_inv[8] = [ + t for sp in sps for rp in repr_periods(sp) for + sc in opscenarios(rp) for t in sc + ] + + for ops in ops_inv + @test length(ops) == n_op + end + for (i, op) in enumerate(ops_inv[1]) + for ops in ops_inv[2:end] + @test op == ops[i] + end + end + + # Test the invariants for operational scenarios + oscs_inv = Array{Any}(nothing, 4) + oscs_inv[1] = [osc for osc in opscenarios(ts)] + oscs_inv[2] = [osc for sp in sps for osc in opscenarios(sp)] + + oscs_inv[3] = + [osc for rp in repr_periods(ts) for osc in opscenarios(rp)] + oscs_inv[4] = [ + osc for sp in sps for rp in repr_periods(sp) for + osc in opscenarios(rp) + ] + + for oscs in oscs_inv + @test length(oscs) == n_sc + end + for (i, osc) in enumerate(oscs_inv[1]) + for oscs in oscs_inv[2:end] + @test osc === oscs[i] + end + end + + # Test the invariants for representative periods + rps_inv = Array{Any}(nothing, 2) + + rps_inv[1] = [rp for rp in repr_periods(ts)] + rps_inv[2] = [rp for sp in sps for rp in repr_periods(sp)] + + for rps in rps_inv + @test length(rps) == n_rp + end + for (i, rp) in enumerate(rps_inv[1]) + for rps in rps_inv[2:end] + @test rp == rps[i] + end + end + + return ops_inv[1] + end +end + +@testitem "TwoLevelTree with SimpleTimes" setup = [TwoLevelTreeTest] begin regtree = TimeStruct.regular_tree(5, [3, 2], SimpleTimes(5, 1)) - ops = [t for n in TimeStruct.strat_nodes(regtree) for t in n] - @test length(ops) == 5 * 10 - ops2 = collect(regtree) - @test ops == ops2 + n_sp = 10 + n_op = n_sp * 5 + ops = TwoLevelTreeTest.fun(regtree, n_sp, n_op) op = ops[31] @test TimeStruct._opscen(op) == 1 @@ -892,20 +970,22 @@ end @test TimeStruct._oper(op) == 1 @test duration(op) == 1 @test probability(op) == 1 / 6 - @test typeof(op) == eltype(typeof(regtree)) + @test op isa eltype(typeof(regtree)) nodes = strat_nodes(regtree) for sp in 1:3 - @test sum(probability(n) for n in nodes if n.sp == sp) ≈ 1.0 + @test sum( + TimeStruct.probability_branch(n) for n in nodes if n.sp == sp + ) ≈ 1.0 end node = nodes[2] @test length(node) == 5 - @test typeof(first(node)) == eltype(typeof(node)) + @test first(node) isa eltype(typeof(node)) leaves = TimeStruct.leaves(regtree) @test length(leaves) == TimeStruct.nleaves(regtree) - scens = collect(TimeStruct.scenarios(regtree)) + scens = collect(TimeStruct.strategic_scenarios(regtree)) @test length(scens[2].nodes) == regtree.len @test scens[3].nodes[1] == regtree.nodes[1] @@ -921,7 +1001,7 @@ end price1 = OperationalProfile([1, 2, 2, 5, 6]) price2 = FixedProfile(4) - dsp = TimeStruct.DynamicStochasticProfile([ + dsp = TimeStruct.StrategicStochasticProfile([ [price1], [price1, price2, price2], [price1, price2, price2, price1, price2, price2], @@ -929,6 +1009,77 @@ end @test dsp[ops[4]] == 5 end +@testitem "TwoLevelTree with OperationalScenarios" setup = [TwoLevelTreeTest] begin + regtree = TimeStruct.regular_tree( + 5, + [3, 2], + OperationalScenarios(3, SimpleTimes(5, 1)), + ) + n_sp = 10 + n_sc = n_sp * 3 + n_op = n_sc * 5 + TwoLevelTreeTest.fun(regtree, n_sp, n_op; n_sc) +end + +@testitem "TwoLevelTree with RepresentativePeriods" setup = [TwoLevelTreeTest] begin + regtree = TimeStruct.regular_tree( + 5, + [3, 2], + RepresentativePeriods(2, 1, SimpleTimes(5, 1)), + ) + n_sp = 10 + n_rp = n_sp * 2 + n_op = n_rp * 5 + TwoLevelTreeTest.fun(regtree, n_sp, n_op; n_rp) +end + +@testitem "TwoLevelTree with RepresentativePeriods and OperationalScenarios" setup = + [TwoLevelTreeTest] begin + regtree = TimeStruct.regular_tree( + 5, + [3, 2], + RepresentativePeriods(2, 1, OperationalScenarios(3, SimpleTimes(5, 1))), + ) + n_sp = 10 + n_rp = n_sp * 2 + n_sc = n_rp * 3 + n_op = n_sc * 5 + TwoLevelTreeTest.fun(regtree, n_sp, n_op; n_sc, n_rp) +end + +@testitem "Strategic scenarios with operational scenarios" begin + regtree = TimeStruct.regular_tree( + 5, + [3, 2], + OperationalScenarios(3, SimpleTimes(5, 1)), + ) + + @test length(TimeStruct.strategic_scenarios(regtree)) == 6 + + for sc in TimeStruct.strategic_scenarios(regtree) + @test length(sc) == length(collect(sc)) + + for (prev_sp, sp) in withprev(sc) + if !isnothing(prev_sp) + @test TimeStruct._strat_per(prev_sp) + 1 == + TimeStruct._strat_per(sp) + end + end + end +end + +@testitem "TwoLevel as a tree" begin + two_level = TwoLevel(5, 10, SimpleTimes(10, 1)) + + scens = TimeStruct.strategic_scenarios(two_level) + @test length(scens) == 1 + sps = collect( + sp for sc in TimeStruct.strategic_scenarios(two_level) for + sp in strat_periods(sc) + ) + @test length(sps) == 5 +end + @testitem "Profiles constructors" begin # Checking the input type @test_throws MethodError FixedProfile("wrong_input") @@ -1024,72 +1175,6 @@ end @test vals == [1, 2, 3, 4, 2, 4, 6, 8, 3, 6, 9, 12] end -@testitem "TwoLevelTree and opscenarios" begin - regtree = TimeStruct.regular_tree( - 5, - [3, 2], - OperationalScenarios(3, SimpleTimes(5, 1)), - ) - - ops1 = collect(regtree) - ops2 = [ - t for sp in strat_periods(regtree) for sc in opscenarios(sp) for t in sc - ] - @test length(ops1) == length(ops2) - for (i, op) in enumerate(ops1) - @test op == ops2[i] - end - - @test sum(length(opscenarios(sp)) for sp in strat_periods(regtree)) == 30 - @test sum( - length(sc) for sp in strat_periods(regtree) for sc in opscenarios(sp) - ) == 150 - - sregtree = TimeStruct.regular_tree(5, [3, 2], SimpleTimes(5, 1)) - ops1 = collect(sregtree) - ops2 = [ - t for sp in strat_periods(sregtree) for sc in opscenarios(sp) for - t in sc - ] - @test length(ops1) == length(ops2) - @test ops1 == ops2 - - @test sum(length(opscenarios(sp)) for sp in strat_periods(sregtree)) == 10 - @test sum( - length(sc) for sp in strat_periods(sregtree) for sc in opscenarios(sp) - ) == 50 -end - -@testitem "Strategic scenarios" begin - regtree = TimeStruct.regular_tree( - 5, - [3, 2], - OperationalScenarios(3, SimpleTimes(5, 1)), - ) - - @test length(scenarios(regtree)) == 6 - - for sc in scenarios(regtree) - @test length(sc) == length(collect(sc)) - - for (prev_sp, sp) in withprev(sc) - if !isnothing(prev_sp) - @test TimeStruct._strat_per(prev_sp) + 1 == - TimeStruct._strat_per(sp) - end - end - end -end - -@testitem "TwoLevel as a tree" begin - two_level = TwoLevel(5, 10, SimpleTimes(10, 1)) - - scens = scenarios(two_level) - @test length(scens) == 1 - sps = collect(sp for sc in scenarios(two_level) for sp in strat_periods(sc)) - @test length(sps) == 5 -end - @testitem "Iteration utilities" begin uniform_day = SimpleTimes(24, 1) uniform_week = TwoLevel(7, 24, uniform_day)