diff --git a/docs/src/manual/multi.md b/docs/src/manual/multi.md index 3592de1..01074b2 100644 --- a/docs/src/manual/multi.md +++ b/docs/src/manual/multi.md @@ -133,6 +133,6 @@ In the example above, if we only allow one investment in the planning period, th ```@repl ts for sc in strategic_scenarios(two_level_tree) - @constraint(m, sum(invest[sp] for sp in sc) <= 1) + @constraint(m, sum(invest[sp] for sp in strat_periods(sc)) <= 1) end ``` diff --git a/docs/src/reference/internal.md b/docs/src/reference/internal.md index 494bfa8..5972578 100644 --- a/docs/src/reference/internal.md +++ b/docs/src/reference/internal.md @@ -14,16 +14,20 @@ TimeStruct.TimeStructOuterIter ```@docs TimeStruct.AbstractTreeNode +TimeStruct.AbstractStrategicScenario TimeStruct.StratNode TimeStruct.StrategicScenario +TimeStruct.SingleStrategicScenario ``` ### [Iterator types](@id int-types-twoleveltree-iter) ```@docs TimeStruct.AbstractTreeStructure +TimeStruct.AbstractStratScens TimeStruct.StratTreeNodes -TimeStruct.StrategicScenarios +TimeStruct.StratScens +TimeStruct.SingleStrategicScenarioWrapper ``` ## [Strategic period types ([`TwoLevel`](@ref))](@id int-types-strat_twolevel) diff --git a/src/TimeStruct.jl b/src/TimeStruct.jl index 52a8d58..857a2d3 100644 --- a/src/TimeStruct.jl +++ b/src/TimeStruct.jl @@ -14,6 +14,7 @@ include("strategic/core_types.jl") include("strategic/strat_periods.jl") include("strat_scenarios/tree_periods.jl") include("strat_scenarios/core_types.jl") +include("strat_scenarios/strat_scenarios.jl") include("representative/rep_periods.jl") include("representative/strat_periods.jl") include("representative/tree_periods.jl") diff --git a/src/op_scenarios/strat_periods.jl b/src/op_scenarios/strat_periods.jl index d632cc6..8513839 100644 --- a/src/op_scenarios/strat_periods.jl +++ b/src/op_scenarios/strat_periods.jl @@ -275,19 +275,9 @@ correct behavior based on the substructure. opscenarios(ts::SingleStrategicPeriod) = opscenarios(ts.ts) """ When the `TimeStructure` is a [`TwoLevel`](@ref), `opscenarios` returns a vector of -[`StratOpScenario`](@ref)s. +[`StratOpScenario`](@ref)s or a a vector of [`StratReprOpScenario`](@ref)s depending on +whether the `TimeStructure` includes [`RepresentativePeriods`](@ref) or not. """ function opscenarios(ts::TwoLevel{S,T,OP}) where {S,T,OP} return collect(Iterators.flatten(opscenarios(sp) for sp in strategic_periods(ts))) end -""" -When the `TimeStructure` is a [`TwoLevel`](@ref) with [`RepresentativePeriods`](@ref), -`opscenarios` returns a vector of [`StratReprOpScenario`](@ref)s. -""" -function opscenarios(ts::TwoLevel{S1,T,RepresentativePeriods{S2,T,OP}}) where {S1,S2,T,OP} - return collect( - Iterators.flatten( - opscenarios(rp) for sp in strategic_periods(ts) for rp in repr_periods(sp) - ), - ) -end diff --git a/src/op_scenarios/tree_periods.jl b/src/op_scenarios/tree_periods.jl index a9e7539..4715b46 100644 --- a/src/op_scenarios/tree_periods.jl +++ b/src/op_scenarios/tree_periods.jl @@ -239,23 +239,15 @@ function opscenarios(n::StratNode{S,T,OP}) where {S,T,OP<:RepresentativePeriods} end """ -When the `TimeStructure` is a [`TwoLevelTree`](@ref), `opscenarios` returns an `Array` of -all [`StratNodeOpScenario`](@ref)s or [`StratNodeReprOpScenario`](@ref)s types, -dependening on whether the [`TwoLevelTree`](@ref) includes [`RepresentativePeriods`](@ref) +When the `TimeStructure` is a [`TwoLevelTree`](@ref), [`StratScens`](@ref), or a +[`StrategicScenario`](@ref), `opscenarios` returns an `Array` of all +[`StratNodeOpScenario`](@ref)s or [`StratNodeReprOpScenario`](@ref)s types, +depending on whether the `TimeStructure` includes [`RepresentativePeriods`](@ref) or not. These are equivalent to a [`StratOpScenario`](@ref) and [`StratReprOpScenario`](@ref) of a [`TwoLevel`](@ref) time structure. """ -function opscenarios(ts::TwoLevelTree) +function opscenarios(ts::Union{TwoLevelTree,StratScens,StrategicScenario}) 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 diff --git a/src/representative/tree_periods.jl b/src/representative/tree_periods.jl index 00ebe19..01b219f 100644 --- a/src/representative/tree_periods.jl +++ b/src/representative/tree_periods.jl @@ -93,11 +93,12 @@ end Base.eltype(_::StratNodeReprPers) = StratNodeReprPeriod """ -When the `TimeStructure` is a [`TwoLevelTree`](@ref), `repr_periods` returns an `Array` of -all [`StratNodeReprPeriod`](@ref)s. +When the `TimeStructure` is a [`TwoLevelTree`](@ref), [`StratScens`](@ref), or a +[`StrategicScenario`](@ref), `repr_periods` returns an `Array` of all +[`StratNodeReprPeriod`](@ref)s. These are equivalent to a [`StratReprPeriod`](@ref) of a [`TwoLevel`](@ref) time structure. """ -function repr_periods(ts::TwoLevelTree) +function repr_periods(ts::Union{TwoLevelTree,StratScens,StrategicScenario}) return collect(Iterators.flatten(repr_periods(sp) for sp in strat_periods(ts))) end diff --git a/src/strat_scenarios/core_types.jl b/src/strat_scenarios/core_types.jl index 7a96750..561d465 100644 --- a/src/strat_scenarios/core_types.jl +++ b/src/strat_scenarios/core_types.jl @@ -193,75 +193,6 @@ function TreePeriod(n::StratNode, per::TimePeriod) mult = n.mult_sp * multiple(per) return TreePeriod(_strat_per(n), _branch(n), per, mult, probability_branch(n)) end -""" - struct StrategicScenario - -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. -""" -struct StrategicScenario - probability::Float64 - nodes::Vector{<:StratNode} -end - -# Iterate through strategic periods of scenario -Base.length(scen::StrategicScenario) = length(scen.nodes) -Base.last(scen::StrategicScenario) = last(scen.nodes) - -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 StrategicScenarios - -Type for iteration through the individual strategic scenarios represented as -[`StrategicScenario`](@ref). -""" -struct StrategicScenarios - ts::TwoLevelTree -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 [`TwoLevel`](@ref), `strategic_scenarios` returns a Vector with -the `TwoLevel` as a single entry. -""" -strategic_scenarios(ts::TwoLevel) = [ts] - -""" -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) = n_leaves(scens.ts) -function Base.iterate(scs::StrategicScenarios, state = 1) - if state > n_leaves(scs.ts) - return nothing - end - - node = get_leaf(scs.ts, state) - prob = probability_branch(node) - nodes = [node] - while !isnothing(_parent(node)) - node = _parent(node) - pushfirst!(nodes, node) - end - - return StrategicScenario(prob, nodes), state + 1 -end """ add_node!( diff --git a/src/strat_scenarios/strat_scenarios.jl b/src/strat_scenarios/strat_scenarios.jl new file mode 100644 index 0000000..a298424 --- /dev/null +++ b/src/strat_scenarios/strat_scenarios.jl @@ -0,0 +1,186 @@ +""" + abstract type AbstractStrategicScenario{T} <: TimeStructurePeriod{T} + +Abstract type used for time structures that represent a strategic scenario. +These periods are obtained when iterating through the strategic scenarios of a time +structure declared by the function [`strategic_scenarios`](@ref). +""" +abstract type AbstractStrategicScenario{T} <: TimeStructurePeriod{T} end + +""" + abstract type AbstractStratScens{S,T} <: TimeStructInnerIter + +Abstract type used for time structures that represent a collection of strategic scenarios, +obtained through calling the function [`strategic_scenarios`](@ref). +""" +abstract type AbstractStratScens{T} <: TimeStructInnerIter{T} end + +""" + struct SingleStrategicScenario{T,SC<:TimeStructure{T}} <: AbstractStrategicScenario{T} + +A type representing a single strategic scenario supporting iteration over its +time periods. It is created when iterating through [`SingleStrategicScenarioWrapper`](@ref). +""" +struct SingleStrategicScenario{T,SC<:TimeStructure{T}} <: AbstractStrategicScenario{T} + ts::SC +end + +# Add basic functions of iterators +Base.length(sc::SingleStrategicScenario) = length(sc.ts) +Base.eltype(::Type{SingleStrategicScenario{T,SC}}) where {T,SC} = eltype(SC) +function Base.iterate(sc::SingleStrategicScenario, state = nothing) + next = isnothing(state) ? iterate(sc.ts) : iterate(sc.ts, state) + return next +end +Base.last(sc::SingleStrategicScenario) = last(sc.ts) + +""" + struct SingleStrategicScenarioWrapper{T,SC<:TimeStructure{T}} <: AbstractStratScens{T} + +Type for iterating through the individual strategic periods of a time structure +without [`TwoLevelTree`](@ref). It is automatically created through the function +[`strategic_scenarios`](@ref). +""" +struct SingleStrategicScenarioWrapper{T,SC<:TimeStructure{T}} <: AbstractStratScens{T} + ts::SC +end + +# Add basic functions of iterators +Base.length(scs::SingleStrategicScenarioWrapper) = 1 +function Base.iterate(scs::SingleStrategicScenarioWrapper, state = nothing) + !isnothing(state) && return nothing + return SingleStrategicScenario(scs.ts), 1 +end +function Base.eltype(::Type{SingleStrategicScenarioWrapper{T,SC}}) where {T,SC} + return SingleStrategicScenario{T,SC} +end +Base.last(scs::SingleStrategicScenarioWrapper) = SingleStrategicScenario(scs.ts) + +""" +When the `TimeStructure` is a [`SingleStrategicScenario`](@ref) or +[`SingleStrategicScenarioWrapper`](@ref), `strat_periods` returns the value of its internal +[`TimeStructure`](@ref). +""" +strat_periods(sc::SingleStrategicScenario) = strat_periods(sc.ts) +strat_periods(sc::SingleStrategicScenarioWrapper) = strat_periods(sc.ts) + +""" + strategic_scenarios(ts::TimeStructure) + +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 +[`SingleStrategicScenarioWrapper`](@ref). This corresponds to the default behavior. +""" +strategic_scenarios(ts::TimeStructure) = SingleStrategicScenarioWrapper(ts) + +""" + struct StrategicScenario{S,T,OP<:AbstractTreeNode{S,T}} <: AbstractStrategicScenario{T} + +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. +""" +struct StrategicScenario{S,T,N,OP<:AbstractTreeNode{S,T}} <: AbstractStrategicScenario{T} + scen::Int64 + probability::Float64 + nodes::NTuple{N,<:OP} + op_per_strat::Float64 +end + +Base.show(io::IO, scen::StrategicScenario) = print(io, "scen$(scen.scen)") + +# Add basic functions of iterators +Base.length(scen::StrategicScenario) = sum(length(sn) for sn in scen.nodes) +Base.last(scen::StrategicScenario) = last(last(scen.nodes)) +Base.eltype(_::Type{StrategicScenario{S,T,N,OP}}) where {S,T,N,OP} = eltype(OP) +function Base.iterate(scs::StrategicScenario, state = (nothing, 1)) + sp = state[2] + next = isnothing(state[1]) ? iterate(scs.nodes[sp]) : iterate(scs.nodes[sp], state[1]) + if isnothing(next) + sp = sp + 1 + if sp > length(scs.nodes) + return nothing + end + next = iterate(scs.nodes[sp]) + end + return next[1], (next[2], sp) +end + +""" +When the `TimeStructure` is a [`StrategicScenario`](@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::StrategicScenario) = StratTreeNodes( + TwoLevelTree(length(ts), first(ts), [n for n in ts.nodes], ts.op_per_strat), +) + +""" + struct StratScens{S,T,OP<:AbstractTreeNode{S,T}} <: AbstractStratScens{T} + +Type for iteration through the individual strategic scenarios represented as +[`StrategicScenario`](@ref). +""" +struct StratScens{S,T,OP<:AbstractTreeNode{S,T}} <: AbstractStratScens{T} + ts::TwoLevelTree{S,T,OP} +end + +""" +When the `TimeStructure` is a [`TwoLevelTree`](@ref), `strategic_scenarios` returns the +iterator `StratScens`. +""" +strategic_scenarios(ts::TwoLevelTree) = StratScens(ts) + +# Provide a constructor to simplify the design +function StrategicScenario( + scs::StratScens{S,T,OP}, + scen::Int, +) where {S,T,OP<:TimeStructure{T}} + node = get_leaf(scs.ts, scen) + prob = probability_branch(node) + n_strat_per = _strat_per(node) + nodes = Vector{OP}(undef, n_strat_per) + for sp in n_strat_per:-1:1 + nodes[sp] = node + node = _parent(node) + end + + return StrategicScenario(scen, prob, Tuple(nodes), scs.ts.op_per_strat) +end + +# Add basic functions of iterators +Base.length(scens::StratScens) = n_leaves(scens.ts) +function Base.eltype(_::StratScens{S,T,OP}) where {S,T,OP<:TimeStructure{T}} + return StrategicScenario +end +function Base.iterate(scs::StratScens, state = nothing) + scen = isnothing(state) ? 1 : state + 1 + scen > n_leaves(scs.ts) && return nothing + + return StrategicScenario(scs, scen), scen +end +function Base.getindex(scs::StratScens, index::Int) + return StrategicScenario(scs, index) +end +function Base.eachindex(scs::StratScens) + return Base.OneTo(n_leaves(scs.ts)) +end +function Base.last(scs::StratScens) + return StrategicScenario(scs, length(scs)) +end + +""" +When the `TimeStructure` is a [`StratScens`](@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. + +!!! note + The corresponding `StratTreeNodes` type is equivalent to the created `StratTreeNodes` + when using `strat_periods` directly on the [`TwoLevelTree`](@ref). +""" +strat_periods(ts::StratScens) = StratTreeNodes(ts.ts) diff --git a/src/strat_scenarios/tree_periods.jl b/src/strat_scenarios/tree_periods.jl index b4ca1d8..a91f149 100644 --- a/src/strat_scenarios/tree_periods.jl +++ b/src/strat_scenarios/tree_periods.jl @@ -64,6 +64,7 @@ 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.last(n::StratNode) = TreePeriod(n, last(n.operational)) Base.eltype(::Type{StratNode{S,T,OP}}) where {S,T,OP} = TreePeriod{eltype(OP)} function Base.iterate(n::StratNode, state = nothing) next = isnothing(state) ? iterate(n.operational) : iterate(n.operational, state) diff --git a/test/runtests.jl b/test/runtests.jl index 1d045f2..9771742 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1159,9 +1159,9 @@ end # Test strategic scenarios scens = collect(strategic_scenarios(regtree)) - @test length(strategic_scenarios(regtree)) == 6 + @test length(strategic_scenarios(regtree)) == n_leaves(regtree) @test length(scens[2].nodes) == regtree.len - @test last(scens[1]) == regtree.nodes[3] + @test last(scens[1]) == last(regtree.nodes[3]) @test scens[3].nodes[1] == regtree.nodes[1] ssp = StrategicStochasticProfile([[10], [11, 12, 13], [20, 21, 22, 23, 30, 40]]) @@ -1210,31 +1210,102 @@ end TwoLevelTreeTest.fun(regtree, n_sp, n_op; n_sc, n_rp) end -@testitem "Strategic scenarios with operational scenarios" begin - regtree = TwoLevelTree(5, [3, 2], OperationalScenarios(3, SimpleTimes(5, 1))) +@testitem "Strategic scenarios with TwoLevelTree" begin + const TS = TimeStruct + oper_scens = OperationalScenarios(3, SimpleTimes(5, 1)) + regtree = TwoLevelTree(5, [3, 2], oper_scens) + scens = strategic_scenarios(regtree) - @test length(TimeStruct.strategic_scenarios(regtree)) == 6 + # Test general functionality + @test eltype(scens) == TS.StrategicScenario + @test length(scens) == 6 + @test last(scens) == scens[6] + @test eachindex(scens) == Base.OneTo(6) - for sc in TimeStruct.strategic_scenarios(regtree) + # Test that the strategic periods are correct + sps = strat_periods(regtree) + sps_scens = strat_periods(scens) + sps_scen_1 = strat_periods(first(scens)) + @test sps == sps_scens + @test all(sps1 === sps2 for (sps1, sps2) in zip(sps_scen_1, collect(sps)[1:3])) + + # Test that the representative periods are correct + rps = repr_periods(regtree) + rps_scens = repr_periods(scens) + rps_scen_1 = repr_periods(first(scens)) + @test rps == rps_scens + @test all(rps1 === rps2 for (rps1, rps2) in zip(rps_scen_1, rps[1:9])) + + # Test that the operational scenarios are correct + oscs = opscenarios(regtree) + oscs_scens = opscenarios(scens) + oscs_scen_1 = opscenarios(first(scens)) + @test oscs == oscs_scens + @test all(oscs1 === oscs2 for (oscs1, oscs2) in zip(oscs_scen_1, oscs[1:3])) + + # Test that the operational scenarios are correct when using representative periods + rep_pers = RepresentativePeriods(20, [0.25, 0.25, 0.25, 0.25], oper_scens) + regtree_rp = TwoLevelTree(5, [3, 2], rep_pers) + scens_rp = strategic_scenarios(regtree_rp) + oscs_rp = opscenarios(regtree_rp) + oscs_rp_scens = opscenarios(scens_rp) + oscs_rp_scen_1 = opscenarios(first(scens_rp)) + @test oscs_rp == oscs_rp_scens + @test all(oscs1 === oscs2 for (oscs1, oscs2) in zip(oscs_rp_scen_1, oscs_rp[1:3])) + + # Test that the tree periods are correct + ops_scen_1 = first(scens) + @test length(ops_scen_1) == 45 + @test last(ops_scen_1) == collect(regtree)[45] + @test all(tp1 === tp2 for (tp1, tp2) in zip(ops_scen_1, collect(regtree)[1:45])) + InnerPeriod = TS.ScenarioPeriod{TS.SimplePeriod{Int64}} + @test isa(first(ops_scen_1), TS.TreePeriod{InnerPeriod}) + + ops_rp_scen_1 = first(scens_rp) + @test length(ops_rp_scen_1) == 180 + @test last(ops_rp_scen_1) == collect(regtree_rp)[180] + @test all(tp1 === tp2 for (tp1, tp2) in zip(ops_rp_scen_1, collect(regtree_rp)[1:180])) + @test isa(first(ops_rp_scen_1), TS.TreePeriod{TS.ReprPeriod{InnerPeriod}}) + + # Test some additional functionality + for (k, sc) in enumerate(scens) @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 + @test repr(sc) == "scen$(k)" + @test eltype(typeof(sc)) == eltype(typeof(regtree)) end end -@testitem "TwoLevel as a tree" begin +@testitem "Strategic scenarios with TwoLevel" begin + const TS = TimeStruct two_level = TwoLevel(5, 10, SimpleTimes(10, 1)) + sps = strat_periods(two_level) + + # Test the Indexing + @test TS.StrategicTreeIndexable(typeof(first(sps))) == TS.NoStratTreeIndex() + @test TS.StrategicTreeIndexable(typeof(first(first(sps)))) == TS.HasStratTreeIndex() - scens = TimeStruct.strategic_scenarios(two_level) + # Test that we get the correct types and that their utilities are working + scens = strategic_scenarios(two_level) + @test isa(scens, TS.SingleStrategicScenarioWrapper{Int64,typeof(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 + @test eltype(scens) == TS.SingleStrategicScenario{Int64,typeof(two_level)} + @test last(scens) == first(scens) + + scen = first(scens) + @test isa(scen, TS.SingleStrategicScenario{Int64,typeof(two_level)}) + @test length(scen) == 10 * 5 + @test eltype(scen) == TS.OperationalPeriod{TS.SimplePeriod{Int64}} + @test last(scen) == last(two_level) + @test all(op1 === op2 for (op1, op2) in zip(scen, two_level)) + + # Test that the iteration utilities are working + @test all(t_scen == t for (t_scen, t) in zip(scen, two_level)) + + # Test the iterators + @test strat_periods(two_level) === strat_periods(scens) + @test strat_periods(two_level) === strat_periods(scen) + @test length(strat_periods(scens)) == 5 + @test length(strat_periods(scen)) == 5 end @testitem "Profiles constructors" begin