From 75e32c280f196a0afba5a3ce315cb501afe5e4b2 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Thu, 11 Jun 2026 09:08:22 +0200 Subject: [PATCH 1/8] Added partition_duration for TwoLevel{SimpleTimes} - To be added for other time structures --- docs/src/reference/api.md | 1 + src/TimeStruct.jl | 2 +- src/utils.jl | 74 +++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 27 ++++++++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) diff --git a/docs/src/reference/api.md b/docs/src/reference/api.md index fac9347..ed74712 100644 --- a/docs/src/reference/api.md +++ b/docs/src/reference/api.md @@ -59,6 +59,7 @@ withprev withnext chunk chunk_duration +partition_duration ``` ## [Properties of time periods](@id api-prop_per) diff --git a/src/TimeStruct.jl b/src/TimeStruct.jl index 857a2d3..a4a0cbb 100644 --- a/src/TimeStruct.jl +++ b/src/TimeStruct.jl @@ -49,7 +49,7 @@ export opscenarios export repr_periods export strat_periods, strategic_periods export regular_tree, strat_nodes, strategic_scenarios -export withprev, withnext, chunk, chunk_duration +export withprev, withnext, chunk, chunk_duration, partition_duration export isfirst, duration, duration_strat, multiple export probability, probability_branch, probability_scen export multiple_strat diff --git a/src/utils.jl b/src/utils.jl index b47df38..2c45949 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -172,6 +172,80 @@ function chunk_duration(_::StratTreeNodes{S,T,OP}, _) where {S,T,OP} ) end +""" + abstract type PartitionDuration{T<:TimePeriod} + +Supertype for individual partitions based on durations for operational time periods. Subtypes +must be created for all potential time structures to be able to identify the respective +[`TimeStructureperiod`](@ref). +""" + +abstract type PartitionDuration{T<:TimePeriod} end + +Base.iterate(pd::PartitionDuration) = iterate(pd.chunk) +Base.iterate(pd::PartitionDuration, state) = iterate(pd.chunk, state) +Base.length(pd::PartitionDuration) = length(pd.chunk) +Base.first(pd::PartitionDuration) = first(pd.chunk) +Base.last(pd::PartitionDuration) = last(pd.chunk) + +struct PartitionDurationIterator{I<:TimeStructure} + itr::I + duration::TimeStruct.Duration +end + +""" + partition_duration(itr, dur) + +Iterator wrapper that yields partitions of time periods where each partition is an iterator +over the following time periods until at least `dur` time is covered or the end is reached. + + +!!! note "Application" + The partitions only cover time periods within an operational scenario, representative + period, or strategic period depending on the chosen time structure. + + The reason for this approach is the lack of meaning a partition of a + [`TimeStructurePeriod`](@ref) +""" +partition_duration(itr, dur) = PartitionDurationIterator(itr, dur) + +IteratorSize(::Type{<:PartitionDurationIterator}) = Base.SizeUnknown() +IteratorEltype(::Type{PartitionDurationIterator{I}}) where {I} = Base.HasEltype() + +function Base.iterate(w::PartitionDurationIterator, state = (nothing, 1)) + isa(state[1], Iterators.IterationCutShort) && return nothing + y = iterate(w.itr, state[1]) + isnothing(y) && return nothing + part = state[2] + chunk = eltype(w.itr)[] + acc = zero(w.duration) + while !isnothing(y) + push!(chunk, y[1]) + acc += duration(y[1]) + acc >= w.duration && break + y = iterate(w.itr, y[2]) + end + pd = PartitionDuration(w.itr, part, Tuple(chunk)) + isnothing(y) && return pd, (Iterators.IterationCutShort(), part+1) + return pd, (y[2], part+1) +end + +struct StratPart{N,T} <: PartitionDuration{T} + sp::Int + part::Int + chunk::NTuple{N,T} +end +PartitionDuration(itr::StrategicPeriod, part, chunk) = StratPart(itr.sp, part, chunk) +eltype(::Type{PartitionDurationIterator{I}}) where {I<:StrategicPeriod} = StratPart + +Base.show(io::IO, pd::StratPart) = print(io, "sp$(pd.sp)-part$(pd.part)") + +function partition_duration(ts::TwoLevel, dur) + return collect( + Iterators.flatten(partition_duration(sp, dur) for sp in strategic_periods(ts)), + ) +end + """ end_oper_time(t, ts) diff --git a/test/runtests.jl b/test/runtests.jl index d2b2319..ec62e08 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1761,6 +1761,33 @@ end for s in sdur @test (sum(duration(t) for t in s) >= 5) || (last(periods) in s) end + + # Test of the iterator utilities when running on a strategic period + ts_ops = SimpleTimes([1, 2, 3, 6, 1, 1, 1, 1, 1, 1]) + ts = TwoLevel(2, 1, ts_ops) + ops = collect(ts) + + sp = first(strategic_periods(ts)) + pds_sp = [pd for pd in partition_duration(sp, 6)] + idx = [1:3, 4:4, 5:10] + + @test typeof(pds_sp[1]) <: TimeStruct.StratPart{3,<:TimeStruct.OperationalPeriod} + @test all(collect(pds_sp[k]) == ops[l] for (k, l) in enumerate(idx)) + @test first(pds_sp[1]) == first(ops) + @test last(pds_sp[1]) == ops[3] + @test length(pds_sp[1]) == 3 + @test repr(pds_sp[3]) == "sp1-part3" + + @test length(pds_sp) == 3 + @test typeof(partition_duration(sp, 6)) <: + TimeStruct.PartitionDurationIterator{<:TimeStruct.StrategicPeriod} + @test eltype(partition_duration(sp, 6)) == TimeStruct.StratPart + @test all(sum(duration(t) for t in pd) >= 6 for pd in pds_sp) + + # Test of the iterator invariants + pds_tl = partition_duration(ts, 6) + @test length(pds_tl) == 6 + @test all(pds_sp[k] == pds_tl[k] for k in 1:3) end @testitem "Indexing of operational structures" begin From c2dd8082aa0c5326b16ab4031d6e1ffa7332711a Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Thu, 11 Jun 2026 10:06:37 +0200 Subject: [PATCH 2/8] First take on PartitionProfile --- src/TimeStruct.jl | 1 + src/profiles.jl | 80 ++++++++++++++++++++++++++++++++++++++++++++--- src/utils.jl | 12 +++++++ test/runtests.jl | 38 ++++++++++++++++++++++ 4 files changed, 127 insertions(+), 4 deletions(-) diff --git a/src/TimeStruct.jl b/src/TimeStruct.jl index a4a0cbb..ccbdc6c 100644 --- a/src/TimeStruct.jl +++ b/src/TimeStruct.jl @@ -40,6 +40,7 @@ export TreeNode export TimeProfile export FixedProfile export OperationalProfile +export PartitionProfile export ScenarioProfile export StrategicProfile export StrategicStochasticProfile diff --git a/src/profiles.jl b/src/profiles.jl index f67d575..8e4b95d 100644 --- a/src/profiles.jl +++ b/src/profiles.jl @@ -30,7 +30,7 @@ end function Base.getindex( fp::FixedProfile, _::T, -) where {T<:Union{TimePeriod,TimeStructurePeriod}} +) where {T<:Union{TimePeriod,TimeStructurePeriod,PartitionDuration}} return fp.val end @@ -87,6 +87,53 @@ function _internal_convert(::Type{T}, op::OperationalProfile{S}) where {T,S} return OperationalProfile(convert.(T, op.vals)) end +""" + PartitionProfile(vals::Vector{T}) where {T} + +Time profile with a value that varies with the operational time period. This profile cannot +be accessed using [`AbstractOperationalScenario`](@ref), [`AbstractRepresentativePeriod`](@ref), +or [`AbstractStrategicPeriod`](@ref). + +If too few values are provided, the last provided value will be repeated. + +## Example +```julia +profile = PartitionProfile([1, 2, 3, 4, 5]) +``` +""" +struct PartitionProfile{T} <: TimeProfile{T} + vals::Vector{T} + function PartitionProfile(vals::Vector{T}) where {T} + if T <: Array + throw( + ArgumentError( + "It is not possible to use a `Vector{<:Array}` as input " * + "to an `PartitionProfile`.", + ), + ) + else + new{T}(vals) + end + end +end + +_internal_convert(::Type{T}, pp::PartitionProfile{T}) where {T} = pp +function _internal_convert(::Type{T}, pp::PartitionProfile{S}) where {T,S} + return PartitionProfile(_internal_convert.(T, pp.vals)) +end + +function _value_lookup(::HasPartIndex, pp::PartitionProfile, period) + return pp.vals[_part(period) > length(pp.vals) ? end : _part(period)] +end + +function _value_lookup(::NoPartIndex, pp::PartitionProfile, period) + return error("Type $(typeof(period)) can not be used as index for a partition profile") +end + +function Base.getindex(pp::PartitionProfile, period::T) where {T<:PartitionDuration} + return _value_lookup(PartitionIndexable(T), pp, period) +end + """ StrategicProfile(vals::Vector{P}) where {T, P<:TimeProfile{T}} StrategicProfile(vals::Vector) @@ -141,7 +188,7 @@ end function Base.getindex( sp::StrategicProfile, period::T, -) where {T<:Union{TimePeriod,TimeStructurePeriod}} +) where {T<:Union{TimePeriod,TimeStructurePeriod,PartitionDuration}} return _value_lookup(StrategicIndexable(T), sp, period) end @@ -356,6 +403,9 @@ import Base: +, -, *, / function +(a::OperationalProfile{T}, b::Number) where {T} return OperationalProfile(a.vals .+ b) end +function +(a::PartitionProfile{T}, b::Number) where {T} + return PartitionProfile(a.vals .+ b) +end function +(a::StrategicProfile{T}, b::Number) where {T} return StrategicProfile(a.vals .+ b) end @@ -373,6 +423,9 @@ end function -(a::OperationalProfile{T}, b::Number) where {T} return OperationalProfile(a.vals .- b) end +function -(a::PartitionProfile{T}, b::Number) where {T} + return PartitionProfile(a.vals .- b) +end function -(a::StrategicProfile{T}, b::Number) where {T} return StrategicProfile(a.vals .- b) end @@ -390,6 +443,9 @@ end function *(a::OperationalProfile{T}, b::Number) where {T} return OperationalProfile(a.vals .* b) end +function *(a::PartitionProfile{T}, b::Number) where {T} + return PartitionProfile(a.vals .* b) +end function *(a::StrategicProfile{T}, b::Number) where {T} return StrategicProfile(a.vals .* b) end @@ -408,6 +464,9 @@ end function /(a::OperationalProfile{T}, b::Number) where {T} return OperationalProfile(a.vals ./ b) end +function /(a::PartitionProfile{T}, b::Number) where {T} + return PartitionProfile(a.vals ./ b) +end function /(a::StrategicProfile{T}, b::Number) where {T} return StrategicProfile(a.vals ./ b) end @@ -423,11 +482,13 @@ end -(a::FixedProfile{T}) where {T} = FixedProfile(-a.val) -(a::OperationalProfile{T}) where {T} = OperationalProfile(-a.vals) +-(a::PartitionProfile{T}) where {T} = PartitionProfile(-a.vals) -(a::StrategicProfile{T}) where {T} = StrategicProfile(-a.vals) -(a::ScenarioProfile{T}) where {T} = ScenarioProfile(-a.vals) -(a::RepresentativeProfile{T}) where {T} = RepresentativeProfile(-a.vals) --(a::StrategicStochasticProfile{T}) where {T} = - StrategicStochasticProfile([.-v for v in a.vals]) +function -(a::StrategicStochasticProfile{T}) where {T} + return StrategicStochasticProfile([.-v for v in a.vals]) +end +(a::TimeProfile{T}) where {T} = a @@ -437,6 +498,9 @@ end function +(a::OperationalProfile{T}, b::OperationalProfile{S}) where {T,S} return OperationalProfile(a.vals .+ b.vals) end +function +(a::PartitionProfile{T}, b::PartitionProfile{S}) where {T,S} + return PartitionProfile(a.vals .+ b.vals) +end function +(a::StrategicProfile{T}, b::StrategicProfile{S}) where {T,S} return StrategicProfile(a.vals .+ b.vals) end @@ -454,6 +518,10 @@ function +(a::StrategicProfile{T}, b::OperationalProfile{S}) where {T,S} return StrategicProfile([p + b for p in a.vals]) end +(a::OperationalProfile{T}, b::StrategicProfile{S}) where {T,S} = b + a +function +(a::StrategicProfile{T}, b::PartitionProfile{S}) where {T,S} + return StrategicProfile([p + b for p in a.vals]) +end ++(a::PartitionProfile{T}, b::StrategicProfile{S}) where {T,S} = b + a function +(a::ScenarioProfile{T}, b::OperationalProfile{S}) where {T,S} return ScenarioProfile([p + b for p in a.vals]) end @@ -467,6 +535,10 @@ function +(a::FixedProfile{T}, b::OperationalProfile{S}) where {T,S} return OperationalProfile(a.val .+ b.vals) end +(a::OperationalProfile{T}, b::FixedProfile{S}) where {T,S} = b + a +function +(a::FixedProfile{T}, b::PartitionProfile{S}) where {T,S} + return PartitionProfile(a.val .+ b.vals) +end ++(a::PartitionProfile{T}, b::FixedProfile{S}) where {T,S} = b + a function +(a::FixedProfile{T}, b::StrategicProfile{S}) where {T,S} return StrategicProfile([a + p for p in b.vals]) end diff --git a/src/utils.jl b/src/utils.jl index 2c45949..c0c0761 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -188,6 +188,14 @@ Base.length(pd::PartitionDuration) = length(pd.chunk) Base.first(pd::PartitionDuration) = first(pd.chunk) Base.last(pd::PartitionDuration) = last(pd.chunk) +abstract type PartitionIndexable end + +struct HasPartIndex <: PartitionIndexable end +struct NoPartIndex <: PartitionIndexable end + +PartitionIndexable(::Type) = NoPartIndex() +PartitionIndexable(::Type{<:PartitionDuration}) = HasPartIndex() + struct PartitionDurationIterator{I<:TimeStructure} itr::I duration::TimeStruct.Duration @@ -239,6 +247,10 @@ PartitionDuration(itr::StrategicPeriod, part, chunk) = StratPart(itr.sp, part, c eltype(::Type{PartitionDurationIterator{I}}) where {I<:StrategicPeriod} = StratPart Base.show(io::IO, pd::StratPart) = print(io, "sp$(pd.sp)-part$(pd.part)") +StrategicIndexable(::Type{<:StratPart}) = HasStratIndex() + +_strat_per(pd::StratPart) = pd.sp +_part(pd::StratPart) = pd.part function partition_duration(ts::TwoLevel, dur) return collect( diff --git a/test/runtests.jl b/test/runtests.jl index ec62e08..81f9edb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1433,6 +1433,22 @@ end @test vals == [1, 2, 3, 4, 2, 4, 6, 8, 3, 6, 9, 12] end +@testitem "Profiles and partitions" begin + ts = TwoLevel(3, 5, SimpleTimes(6, 2)) + + pp = PartitionProfile([3, 1, 2]) + vals = collect(pp[pd] for pd in partition_duration(ts, 4)) + @test vals == [3, 1, 2, 3, 1, 2, 3, 1, 2] + + sp1 = StrategicProfile([3, 1, 2]) + vals = collect(sp1[pd] for pd in partition_duration(ts, 4)) + @test vals == [3, 3, 3, 1, 1, 1, 2, 2, 2] + + sp2 = StrategicProfile([pp, pp+10, pp+20]) + vals = collect(sp2[pd] for pd in partition_duration(ts, 4)) + @test vals == [3, 1, 2, 13, 11, 12, 23, 21, 22] +end + @testitem "Profile conversion" begin fp = FixedProfile(12) @test TimeStruct._internal_convert(Int64, fp) == FixedProfile(12) @@ -1498,6 +1514,13 @@ end profile = -OperationalProfile([1, 2, 3]) @test profile.vals == [-1, -2, -3] + # PartitionProfile + profile = +PartitionProfile([1, 2, 3]) + @test profile.vals == [1, 2, 3] + + profile = -PartitionProfile([1, 2, 3]) + @test profile.vals == [-1, -2, -3] + # StrategicProfile simple = SimpleTimes(10, 1) ts = TwoLevel(3, 5, simple) @@ -1548,6 +1571,7 @@ end @testitem "Profile addition and subtraction" begin day = SimpleTimes(5, 1) ts_strat = TwoLevel(3, 10, SimpleTimes(5, 1)) + ts_part = partition_duration(ts_strat, 2) ts_scen = TwoLevel(2, 10, OperationalScenarios(3, SimpleTimes(5, 1))) ts_repr = TwoLevel( 2, @@ -1559,6 +1583,8 @@ end fp2 = FixedProfile(10) op = OperationalProfile([1, 2, 3, 4, 5]) op2 = OperationalProfile([6, 7, 8, 9, 10]) + pp = PartitionProfile([1, 2, 3]) + pp2 = PartitionProfile([6, 7, 8]) sp = StrategicProfile([ OperationalProfile([1, 2, 3, 4, 5]), OperationalProfile([6, 7, 8, 9, 10]), @@ -1589,6 +1615,14 @@ end @test all((fp+op)[t] == fp[t] + op[t] for t in day) @test all((fp+op)[t] == (op+fp)[t] for t in day) + # PartitionProfile + PartitionProfile + @test all((pp+pp2)[t] == pp[t] + pp2[t] for t in ts_part) + @test all((pp+pp2)[t] == (pp2+pp)[t] for t in ts_part) + + # FixedProfile + PartitionProfile + @test all((fp+pp)[t] == fp[t] + pp[t] for t in ts_part) + @test all((fp+pp)[t] == (pp+fp)[t] for t in ts_part) + # StrategicProfile + StrategicProfile @test all((sp+sp2)[t] == sp[t] + sp2[t] for t in ts_strat) @test all((sp+sp2)[t] == (sp2+sp)[t] for t in ts_strat) @@ -1597,6 +1631,10 @@ end @test all((sp+op)[t] == sp[t] + op[t] for t in ts_strat) @test all((sp+op)[t] == (op+sp)[t] for t in ts_strat) + # StrategicProfile + PartitionProfile + @test all((sp2+pp)[t] == sp2[t] + pp[t] for t in ts_part) + @test all((sp2+pp)[t] == (pp+sp2)[t] for t in ts_part) + # FixedProfile + StrategicProfile @test all((fp+sp)[t] == fp[t] + sp[t] for t in ts_strat) @test all((fp+sp)[t] == (sp+fp)[t] for t in ts_strat) From 03f8f77a7c809c90cb8f0aad37ed0c2cb3331422 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Thu, 11 Jun 2026 11:29:33 +0200 Subject: [PATCH 3/8] Added support for TwoLevel and RepresentativePeriods --- src/profiles.jl | 6 +++- src/utils.jl | 21 ++++++++++++++ test/runtests.jl | 74 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 99 insertions(+), 2 deletions(-) diff --git a/src/profiles.jl b/src/profiles.jl index 8e4b95d..1f440a6 100644 --- a/src/profiles.jl +++ b/src/profiles.jl @@ -308,7 +308,7 @@ end function Base.getindex( rp::RepresentativeProfile, period::T, -) where {T<:Union{TimePeriod,TimeStructurePeriod}} +) where {T<:Union{TimePeriod,TimeStructurePeriod,PartitionDuration}} return _value_lookup(RepresentativeIndexable(T), rp, period) end @@ -530,6 +530,10 @@ function +(a::RepresentativeProfile{T}, b::OperationalProfile{S}) where {T,S} return RepresentativeProfile([p + b for p in a.vals]) end +(a::OperationalProfile{T}, b::RepresentativeProfile{S}) where {T,S} = b + a +function +(a::RepresentativeProfile{T}, b::PartitionProfile{S}) where {T,S} + return RepresentativeProfile([p + b for p in a.vals]) +end ++(a::PartitionProfile{T}, b::RepresentativeProfile{S}) where {T,S} = b + a function +(a::FixedProfile{T}, b::OperationalProfile{S}) where {T,S} return OperationalProfile(a.val .+ b.vals) diff --git a/src/utils.jl b/src/utils.jl index c0c0761..9fdee62 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -252,11 +252,32 @@ StrategicIndexable(::Type{<:StratPart}) = HasStratIndex() _strat_per(pd::StratPart) = pd.sp _part(pd::StratPart) = pd.part +struct StratReprPart{N,T} <: PartitionDuration{T} + sp::Int + rp::Int + part::Int + chunk::NTuple{N,T} +end +PartitionDuration(itr::StratReprPeriod, part, chunk) = + StratReprPart(itr.sp, itr.rp, part, chunk) +eltype(::Type{PartitionDurationIterator{I}}) where {I<:StratReprPeriod} = StratReprPart + +Base.show(io::IO, pd::StratReprPart) = print(io, "sp$(pd.sp)-rp$(pd.rp)-part$(pd.part)") +StrategicIndexable(::Type{<:StratReprPart}) = HasStratIndex() +RepresentativeIndexable(::Type{<:StratReprPart}) = HasReprIndex() + +_strat_per(pd::StratReprPart) = pd.sp +_rper(pd::StratReprPart) = pd.rp +_part(pd::StratReprPart) = pd.part + function partition_duration(ts::TwoLevel, dur) return collect( Iterators.flatten(partition_duration(sp, dur) for sp in strategic_periods(ts)), ) end +function partition_duration(ts::StrategicPeriod{S,T,OP}, dur) where {S, T, OP<:RepresentativePeriods} + return collect(Iterators.flatten(partition_duration(rp, dur) for rp in repr_periods(ts))) +end """ end_oper_time(t, ts) diff --git a/test/runtests.jl b/test/runtests.jl index 81f9edb..9f9787a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1447,6 +1447,40 @@ end sp2 = StrategicProfile([pp, pp+10, pp+20]) vals = collect(sp2[pd] for pd in partition_duration(ts, 4)) @test vals == [3, 1, 2, 13, 11, 12, 23, 21, 22] + + rps = RepresentativePeriods(2, 1, SimpleTimes(6, 2)) + ts = TwoLevel(2, 1, rps) + + vals = collect(pp[pd] for pd in partition_duration(ts, 4)) + @test vals == [3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2] + + rp1 = RepresentativeProfile([3, 1]) + vals = collect(rp1[pd] for pd in partition_duration(ts, 4)) + @test vals == [3, 3, 3, 1, 1, 1, 3, 3, 3, 1, 1, 1] + + rp2 = RepresentativeProfile([pp, pp+10]) + vals = collect(rp2[pd] for pd in partition_duration(ts, 4)) + @test vals == [3, 1, 2, 13, 11, 12, 3, 1, 2, 13, 11, 12] + + sp1 = StrategicProfile([3, 1]) + vals = collect(sp1[pd] for pd in partition_duration(ts, 4)) + @test vals == [3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1] + + sp2 = StrategicProfile([3, 1]) + vals = collect(sp2[pd] for pd in partition_duration(ts, 4)) + @test vals == [3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1] + + sp3 = StrategicProfile([pp, pp+100]) + vals = collect(sp3[pd] for pd in partition_duration(ts, 4)) + @test vals == [3, 1, 2, 3, 1, 2, 103, 101, 102, 103, 101, 102] + + sp4 = StrategicProfile([rp1, rp1+100]) + vals = collect(sp4[pd] for pd in partition_duration(ts, 4)) + @test vals == [3, 3, 3, 1, 1, 1, 103, 103, 103, 101, 101, 101] + + sp5 = StrategicProfile([rp2, rp2+100]) + vals = collect(sp5[pd] for pd in partition_duration(ts, 4)) + @test vals == [3, 1, 2, 13, 11, 12, 103, 101, 102, 113, 111, 112] end @testitem "Profile conversion" begin @@ -1578,6 +1612,7 @@ end 5, RepresentativePeriods(2, 5, [0.5, 0.5], [SimpleTimes(5, 1), SimpleTimes(5, 1)]), ) + ts_repr_part = partition_duration(ts_repr, 2) fp = FixedProfile(3) fp2 = FixedProfile(10) @@ -1659,6 +1694,10 @@ end @test all((rp+op)[t] == rp[t] + op[t] for t in ts_repr) @test all((rp+op)[t] == (op+rp)[t] for t in ts_repr) + # RepresentativeProfile + PartitionProfile + @test all((rp2+pp)[t] == rp2[t] + pp[t] for t in ts_repr_part) + @test all((rp2+pp)[t] == (pp+rp2)[t] for t in ts_repr_part) + # FixedProfile + RepresentativeProfile @test all((fp+rp)[t] == fp[t] + rp[t] for t in ts_repr) @test all((fp+rp)[t] == (rp+fp)[t] for t in ts_repr) @@ -1800,7 +1839,7 @@ end @test (sum(duration(t) for t in s) >= 5) || (last(periods) in s) end - # Test of the iterator utilities when running on a strategic period + # Test of the iterator utilities when running on a strategic periods ts_ops = SimpleTimes([1, 2, 3, 6, 1, 1, 1, 1, 1, 1]) ts = TwoLevel(2, 1, ts_ops) ops = collect(ts) @@ -1826,6 +1865,39 @@ end pds_tl = partition_duration(ts, 6) @test length(pds_tl) == 6 @test all(pds_sp[k] == pds_tl[k] for k in 1:3) + + # Test of the iterator utilities when running on a strategic periods with representative + # periods + ts_ops = SimpleTimes([1, 2, 3, 6, 1, 1, 1, 1, 1, 1]) + rps = RepresentativePeriods(2, 1, ts_ops) + ts = TwoLevel(2, 1, rps) + ops = collect(ts) + + sp = first(strategic_periods(ts)) + rp = first(repr_periods(sp)) + pds_rp = [pd for pd in partition_duration(rp, 6)] + idx = [1:3, 4:4, 5:10] + + @test typeof(pds_rp[1]) <: TimeStruct.StratReprPart{3,<:TimeStruct.OperationalPeriod} + @test all(collect(pds_rp[k]) == ops[l] for (k, l) in enumerate(idx)) + @test first(pds_rp[1]) == first(ops) + @test last(pds_rp[1]) == ops[3] + @test length(pds_rp[1]) == 3 + @test repr(pds_rp[3]) == "sp1-rp1-part3" + + @test length(pds_rp) == 3 + @test typeof(partition_duration(rp, 6)) <: + TimeStruct.PartitionDurationIterator{<:TimeStruct.StratReprPeriod} + @test eltype(partition_duration(rp, 6)) == TimeStruct.StratReprPart + @test all(sum(duration(t) for t in pd) >= 6 for pd in pds_rp) + + # Test of the iterator invariants + pds_tl = partition_duration(ts, 6) + pds_sp = [pd for pd in partition_duration(sp, 6)] + @test length(pds_tl) == 12 + @test length(pds_sp) == 6 + @test all(pds_sp[k] == pds_tl[k] for k in 1:3) + @test all(pds_rp[k] == pds_tl[k] for k in 1:3) end @testitem "Indexing of operational structures" begin From a1cdb8a3b184c7b62c3cd7f2252e23e63f031203 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Thu, 11 Jun 2026 12:23:38 +0200 Subject: [PATCH 4/8] Fixed formatting and docs --- docs/src/reference/api.md | 1 + src/utils.jl | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/src/reference/api.md b/docs/src/reference/api.md index ed74712..c8527ec 100644 --- a/docs/src/reference/api.md +++ b/docs/src/reference/api.md @@ -79,6 +79,7 @@ end_oper_time ```@docs FixedProfile OperationalProfile +PartitionProfile RepresentativeProfile ScenarioProfile StrategicProfile diff --git a/src/utils.jl b/src/utils.jl index 9fdee62..6482f59 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -258,8 +258,9 @@ struct StratReprPart{N,T} <: PartitionDuration{T} part::Int chunk::NTuple{N,T} end -PartitionDuration(itr::StratReprPeriod, part, chunk) = - StratReprPart(itr.sp, itr.rp, part, chunk) +function PartitionDuration(itr::StratReprPeriod, part, chunk) + return StratReprPart(itr.sp, itr.rp, part, chunk) +end eltype(::Type{PartitionDurationIterator{I}}) where {I<:StratReprPeriod} = StratReprPart Base.show(io::IO, pd::StratReprPart) = print(io, "sp$(pd.sp)-rp$(pd.rp)-part$(pd.part)") @@ -275,8 +276,13 @@ function partition_duration(ts::TwoLevel, dur) Iterators.flatten(partition_duration(sp, dur) for sp in strategic_periods(ts)), ) end -function partition_duration(ts::StrategicPeriod{S,T,OP}, dur) where {S, T, OP<:RepresentativePeriods} - return collect(Iterators.flatten(partition_duration(rp, dur) for rp in repr_periods(ts))) +function partition_duration( + ts::StrategicPeriod{S,T,OP}, + dur, +) where {S,T,OP<:RepresentativePeriods} + return collect( + Iterators.flatten(partition_duration(rp, dur) for rp in repr_periods(ts)), + ) end """ From 66cf9b0a649cd0b27c88f60969c32e81e0d5389d Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Thu, 11 Jun 2026 12:46:02 +0200 Subject: [PATCH 5/8] Added support for TwoLevel and OperationalScenarios --- src/profiles.jl | 6 +++- src/utils.jl | 27 ++++++++++++++++++ test/runtests.jl | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src/profiles.jl b/src/profiles.jl index 1f440a6..ee94740 100644 --- a/src/profiles.jl +++ b/src/profiles.jl @@ -249,7 +249,7 @@ end function Base.getindex( sp::ScenarioProfile, period::T, -) where {T<:Union{TimePeriod,TimeStructurePeriod}} +) where {T<:Union{TimePeriod,TimeStructurePeriod,PartitionDuration}} return _value_lookup(ScenarioIndexable(T), sp, period) end @@ -526,6 +526,10 @@ function +(a::ScenarioProfile{T}, b::OperationalProfile{S}) where {T,S} return ScenarioProfile([p + b for p in a.vals]) end +(a::OperationalProfile{T}, b::ScenarioProfile{S}) where {T,S} = b + a +function +(a::ScenarioProfile{T}, b::PartitionProfile{S}) where {T,S} + return ScenarioProfile([p + b for p in a.vals]) +end ++(a::PartitionProfile{T}, b::ScenarioProfile{S}) where {T,S} = b + a function +(a::RepresentativeProfile{T}, b::OperationalProfile{S}) where {T,S} return RepresentativeProfile([p + b for p in a.vals]) end diff --git a/src/utils.jl b/src/utils.jl index 6482f59..f1b0dd8 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -271,6 +271,25 @@ _strat_per(pd::StratReprPart) = pd.sp _rper(pd::StratReprPart) = pd.rp _part(pd::StratReprPart) = pd.part +struct StratOpScenPart{N,T} <: PartitionDuration{T} + sp::Int + scen::Int + part::Int + chunk::NTuple{N,T} +end +function PartitionDuration(itr::StratOpScenario, part, chunk) + return StratOpScenPart(itr.sp, itr.scen, part, chunk) +end +eltype(::Type{PartitionDurationIterator{I}}) where {I<:StratOpScenario} = StratOpScenPart + +Base.show(io::IO, pd::StratOpScenPart) = print(io, "sp$(pd.sp)-sc$(pd.scen)-part$(pd.part)") +StrategicIndexable(::Type{<:StratOpScenPart}) = HasStratIndex() +ScenarioIndexable(::Type{<:StratOpScenPart}) = HasScenarioIndex() + +_strat_per(pd::StratOpScenPart) = pd.sp +_opscen(pd::StratOpScenPart) = pd.scen +_part(pd::StratOpScenPart) = pd.part + function partition_duration(ts::TwoLevel, dur) return collect( Iterators.flatten(partition_duration(sp, dur) for sp in strategic_periods(ts)), @@ -284,6 +303,14 @@ function partition_duration( Iterators.flatten(partition_duration(rp, dur) for rp in repr_periods(ts)), ) end +function partition_duration( + ts::StrategicPeriod{S,T,OP}, + dur, +) where {S,T,OP<:OperationalScenarios} + return collect( + Iterators.flatten(partition_duration(osc, dur) for osc in opscenarios(ts)), + ) +end """ end_oper_time(t, ts) diff --git a/test/runtests.jl b/test/runtests.jl index 9f9787a..3817c4b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1448,6 +1448,42 @@ end vals = collect(sp2[pd] for pd in partition_duration(ts, 4)) @test vals == [3, 1, 2, 13, 11, 12, 23, 21, 22] + # Tests for time structure with operational scenarios + oscs = OperationalScenarios(2, SimpleTimes(6, 2)) + ts = TwoLevel(2, 1, oscs) + + vals = collect(pp[pd] for pd in partition_duration(ts, 4)) + @test vals == [3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2] + + scp1 = ScenarioProfile([3, 1]) + vals = collect(scp1[pd] for pd in partition_duration(ts, 4)) + @test vals == [3, 3, 3, 1, 1, 1, 3, 3, 3, 1, 1, 1] + + scp2 = ScenarioProfile([pp, pp+10]) + vals = collect(scp2[pd] for pd in partition_duration(ts, 4)) + @test vals == [3, 1, 2, 13, 11, 12, 3, 1, 2, 13, 11, 12] + + sp1 = StrategicProfile([3, 1]) + vals = collect(sp1[pd] for pd in partition_duration(ts, 4)) + @test vals == [3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1] + + sp2 = StrategicProfile([3, 1]) + vals = collect(sp2[pd] for pd in partition_duration(ts, 4)) + @test vals == [3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1] + + sp3 = StrategicProfile([pp, pp+100]) + vals = collect(sp3[pd] for pd in partition_duration(ts, 4)) + @test vals == [3, 1, 2, 3, 1, 2, 103, 101, 102, 103, 101, 102] + + sp4 = StrategicProfile([scp1, scp1+100]) + vals = collect(sp4[pd] for pd in partition_duration(ts, 4)) + @test vals == [3, 3, 3, 1, 1, 1, 103, 103, 103, 101, 101, 101] + + sp5 = StrategicProfile([scp2, scp2+100]) + vals = collect(sp5[pd] for pd in partition_duration(ts, 4)) + @test vals == [3, 1, 2, 13, 11, 12, 103, 101, 102, 113, 111, 112] + + # Tests for time structure with representative periods rps = RepresentativePeriods(2, 1, SimpleTimes(6, 2)) ts = TwoLevel(2, 1, rps) @@ -1613,6 +1649,7 @@ end RepresentativePeriods(2, 5, [0.5, 0.5], [SimpleTimes(5, 1), SimpleTimes(5, 1)]), ) ts_repr_part = partition_duration(ts_repr, 2) + ts_scen_part = partition_duration(ts_scen, 2) fp = FixedProfile(3) fp2 = FixedProfile(10) @@ -1682,6 +1719,10 @@ end @test all((scp+op)[t] == scp[t] + op[t] for t in ts_scen) @test all((scp+op)[t] == (op+scp)[t] for t in ts_scen) + # ScenarioProfile + OperationalProfile + @test all((scp2+pp)[t] == scp2[t] + pp[t] for t in ts_scen_part) + @test all((scp2+pp)[t] == (pp+scp2)[t] for t in ts_scen_part) + # FixedProfile + ScenarioProfile @test all((fp+scp)[t] == fp[t] + scp[t] for t in ts_scen) @test all((fp+scp)[t] == (scp+fp)[t] for t in ts_scen) @@ -1866,6 +1907,39 @@ end @test length(pds_tl) == 6 @test all(pds_sp[k] == pds_tl[k] for k in 1:3) + # Test of the iterator utilities when running on a strategic periods with operational + # scenarios + ts_ops = SimpleTimes([1, 2, 3, 6, 1, 1, 1, 1, 1, 1]) + oscs = OperationalScenarios(2, ts_ops) + ts = TwoLevel(2, 1, oscs) + ops = collect(ts) + + sp = first(strategic_periods(ts)) + osc = first(opscenarios(sp)) + psc_osc = [pd for pd in partition_duration(osc, 6)] + idx = [1:3, 4:4, 5:10] + + @test typeof(psc_osc[1]) <: TimeStruct.StratOpScenPart{3,<:TimeStruct.OperationalPeriod} + @test all(collect(psc_osc[k]) == ops[l] for (k, l) in enumerate(idx)) + @test first(psc_osc[1]) == first(ops) + @test last(psc_osc[1]) == ops[3] + @test length(psc_osc[1]) == 3 + @test repr(psc_osc[3]) == "sp1-sc1-part3" + + @test length(psc_osc) == 3 + @test typeof(partition_duration(osc, 6)) <: + TimeStruct.PartitionDurationIterator{<:TimeStruct.StratOpScenario} + @test eltype(partition_duration(osc, 6)) == TimeStruct.StratOpScenPart + @test all(sum(duration(t) for t in pd) >= 6 for pd in psc_osc) + + # Test of the iterator invariants + pds_tl = partition_duration(ts, 6) + pds_sp = [pd for pd in partition_duration(sp, 6)] + @test length(pds_tl) == 12 + @test length(pds_sp) == 6 + @test all(pds_sp[k] == pds_tl[k] for k in 1:3) + @test all(psc_osc[k] == pds_tl[k] for k in 1:3) + # Test of the iterator utilities when running on a strategic periods with representative # periods ts_ops = SimpleTimes([1, 2, 3, 6, 1, 1, 1, 1, 1, 1]) From 84f559fcd173aeeb07a562d2a61f7abea1b5811e Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Thu, 11 Jun 2026 13:33:32 +0200 Subject: [PATCH 6/8] Restructured the folder --- src/TimeStruct.jl | 4 ++ src/partitions/opscenarios.jl | 5 +++ src/partitions/rep_periods.jl | 5 +++ src/partitions/strat_periods.jl | 75 ++++++++++++++++++++++++++++++++ src/utils.jl | 76 +-------------------------------- 5 files changed, 91 insertions(+), 74 deletions(-) create mode 100644 src/partitions/opscenarios.jl create mode 100644 src/partitions/rep_periods.jl create mode 100644 src/partitions/strat_periods.jl diff --git a/src/TimeStruct.jl b/src/TimeStruct.jl index ccbdc6c..1b3ed3b 100644 --- a/src/TimeStruct.jl +++ b/src/TimeStruct.jl @@ -27,6 +27,10 @@ include("utils.jl") include("discount.jl") include("profiles.jl") +include("partitions/strat_periods.jl") +include("partitions/rep_periods.jl") +include("partitions/opscenarios.jl") + export TimeStructure export SimpleTimes export CalendarTimes diff --git a/src/partitions/opscenarios.jl b/src/partitions/opscenarios.jl new file mode 100644 index 0000000..3fb5ae7 --- /dev/null +++ b/src/partitions/opscenarios.jl @@ -0,0 +1,5 @@ +# Add generic partition duration type +abstract type AbstractOpScenPart{T} <: PartitionDuration{T} end + +ScenarioIndexable(::Type{<:AbstractOpScenPart}) = HasScenarioIndex() +_opscen(pd::AbstractOpScenPart) = pd.scen diff --git a/src/partitions/rep_periods.jl b/src/partitions/rep_periods.jl new file mode 100644 index 0000000..064b1cb --- /dev/null +++ b/src/partitions/rep_periods.jl @@ -0,0 +1,5 @@ +# Add generic partition duration type +abstract type AbstractReprPart{T} <: PartitionDuration{T} end + +RepresentativeIndexable(::Type{<:AbstractReprPart}) = HasReprIndex() +_rper(pd::AbstractReprPart) = pd.rp diff --git a/src/partitions/strat_periods.jl b/src/partitions/strat_periods.jl new file mode 100644 index 0000000..bb6f7bb --- /dev/null +++ b/src/partitions/strat_periods.jl @@ -0,0 +1,75 @@ +# Add generic partition duration type +abstract type AbstractStratPart{T} <: PartitionDuration{T} end + +StrategicIndexable(::Type{<:AbstractStratPart}) = HasStratIndex() +_strat_per(pd::AbstractStratPart) = pd.sp + +# Add partition type with constructor for indexing when strategic periods are present +struct StratPart{N,T} <: AbstractStratPart{T} + sp::Int + part::Int + chunk::NTuple{N,T} +end +PartitionDuration(itr::StrategicPeriod, part, chunk) = StratPart(itr.sp, part, chunk) +eltype(::Type{PartitionDurationIterator{I}}) where {I<:StrategicPeriod} = StratPart + +Base.show(io::IO, pd::StratPart) = print(io, "sp$(pd.sp)-part$(pd.part)") + +# Add partition type with constructor for indexing when strategic and representative periods +# are present +struct StratReprPart{N,T} <: AbstractStratPart{T} + sp::Int + rp::Int + part::Int + chunk::NTuple{N,T} +end +function PartitionDuration(itr::StratReprPeriod, part, chunk) + return StratReprPart(itr.sp, itr.rp, part, chunk) +end +eltype(::Type{PartitionDurationIterator{I}}) where {I<:StratReprPeriod} = StratReprPart + +Base.show(io::IO, pd::StratReprPart) = print(io, "sp$(pd.sp)-rp$(pd.rp)-part$(pd.part)") +RepresentativeIndexable(::Type{<:StratReprPart}) = HasReprIndex() +_rper(pd::StratReprPart) = pd.rp + +# Add partition type with constructor for indexing when strategic periods and operational +# scenarios are present +struct StratOpScenPart{N,T} <: AbstractStratPart{T} + sp::Int + scen::Int + part::Int + chunk::NTuple{N,T} +end +function PartitionDuration(itr::StratOpScenario, part, chunk) + return StratOpScenPart(itr.sp, itr.scen, part, chunk) +end +eltype(::Type{PartitionDurationIterator{I}}) where {I<:StratOpScenario} = StratOpScenPart + +function Base.show(io::IO, pd::StratOpScenPart) + return print(io, "sp$(pd.sp)-sc$(pd.scen)-part$(pd.part)") +end +ScenarioIndexable(::Type{<:StratOpScenPart}) = HasScenarioIndex() +_opscen(pd::StratOpScenPart) = pd.scen + +# Add function for generation of partitions from higher level +function partition_duration(ts::TwoLevel, dur) + return collect( + Iterators.flatten(partition_duration(sp, dur) for sp in strategic_periods(ts)), + ) +end +function partition_duration( + ts::StrategicPeriod{S,T,OP}, + dur, +) where {S,T,OP<:RepresentativePeriods} + return collect( + Iterators.flatten(partition_duration(rp, dur) for rp in repr_periods(ts)), + ) +end +function partition_duration( + ts::StrategicPeriod{S,T,OP}, + dur, +) where {S,T,OP<:OperationalScenarios} + return collect( + Iterators.flatten(partition_duration(osc, dur) for osc in opscenarios(ts)), + ) +end diff --git a/src/utils.jl b/src/utils.jl index f1b0dd8..f536d6e 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -188,6 +188,8 @@ Base.length(pd::PartitionDuration) = length(pd.chunk) Base.first(pd::PartitionDuration) = first(pd.chunk) Base.last(pd::PartitionDuration) = last(pd.chunk) +_part(pd::PartitionDuration) = pd.part + abstract type PartitionIndexable end struct HasPartIndex <: PartitionIndexable end @@ -238,80 +240,6 @@ function Base.iterate(w::PartitionDurationIterator, state = (nothing, 1)) return pd, (y[2], part+1) end -struct StratPart{N,T} <: PartitionDuration{T} - sp::Int - part::Int - chunk::NTuple{N,T} -end -PartitionDuration(itr::StrategicPeriod, part, chunk) = StratPart(itr.sp, part, chunk) -eltype(::Type{PartitionDurationIterator{I}}) where {I<:StrategicPeriod} = StratPart - -Base.show(io::IO, pd::StratPart) = print(io, "sp$(pd.sp)-part$(pd.part)") -StrategicIndexable(::Type{<:StratPart}) = HasStratIndex() - -_strat_per(pd::StratPart) = pd.sp -_part(pd::StratPart) = pd.part - -struct StratReprPart{N,T} <: PartitionDuration{T} - sp::Int - rp::Int - part::Int - chunk::NTuple{N,T} -end -function PartitionDuration(itr::StratReprPeriod, part, chunk) - return StratReprPart(itr.sp, itr.rp, part, chunk) -end -eltype(::Type{PartitionDurationIterator{I}}) where {I<:StratReprPeriod} = StratReprPart - -Base.show(io::IO, pd::StratReprPart) = print(io, "sp$(pd.sp)-rp$(pd.rp)-part$(pd.part)") -StrategicIndexable(::Type{<:StratReprPart}) = HasStratIndex() -RepresentativeIndexable(::Type{<:StratReprPart}) = HasReprIndex() - -_strat_per(pd::StratReprPart) = pd.sp -_rper(pd::StratReprPart) = pd.rp -_part(pd::StratReprPart) = pd.part - -struct StratOpScenPart{N,T} <: PartitionDuration{T} - sp::Int - scen::Int - part::Int - chunk::NTuple{N,T} -end -function PartitionDuration(itr::StratOpScenario, part, chunk) - return StratOpScenPart(itr.sp, itr.scen, part, chunk) -end -eltype(::Type{PartitionDurationIterator{I}}) where {I<:StratOpScenario} = StratOpScenPart - -Base.show(io::IO, pd::StratOpScenPart) = print(io, "sp$(pd.sp)-sc$(pd.scen)-part$(pd.part)") -StrategicIndexable(::Type{<:StratOpScenPart}) = HasStratIndex() -ScenarioIndexable(::Type{<:StratOpScenPart}) = HasScenarioIndex() - -_strat_per(pd::StratOpScenPart) = pd.sp -_opscen(pd::StratOpScenPart) = pd.scen -_part(pd::StratOpScenPart) = pd.part - -function partition_duration(ts::TwoLevel, dur) - return collect( - Iterators.flatten(partition_duration(sp, dur) for sp in strategic_periods(ts)), - ) -end -function partition_duration( - ts::StrategicPeriod{S,T,OP}, - dur, -) where {S,T,OP<:RepresentativePeriods} - return collect( - Iterators.flatten(partition_duration(rp, dur) for rp in repr_periods(ts)), - ) -end -function partition_duration( - ts::StrategicPeriod{S,T,OP}, - dur, -) where {S,T,OP<:OperationalScenarios} - return collect( - Iterators.flatten(partition_duration(osc, dur) for osc in opscenarios(ts)), - ) -end - """ end_oper_time(t, ts) From 9ed65f4d867e4e39d38047dfe1b134e5eb519cd7 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Thu, 11 Jun 2026 15:05:14 +0200 Subject: [PATCH 7/8] Added support for TwoLevel with OperationalScenarios and RepresentativePeriods --- src/partitions/strat_periods.jl | 30 +++++ test/runtests.jl | 224 +++++++++++++++++++------------- 2 files changed, 165 insertions(+), 89 deletions(-) diff --git a/src/partitions/strat_periods.jl b/src/partitions/strat_periods.jl index bb6f7bb..12c085f 100644 --- a/src/partitions/strat_periods.jl +++ b/src/partitions/strat_periods.jl @@ -51,6 +51,28 @@ end ScenarioIndexable(::Type{<:StratOpScenPart}) = HasScenarioIndex() _opscen(pd::StratOpScenPart) = pd.scen +struct StratReprOpScenPart{N,T} <: AbstractStratPart{T} + sp::Int + rp::Int + scen::Int + part::Int + chunk::NTuple{N,T} +end +function PartitionDuration(itr::StratReprOpScenario, part, chunk) + return StratReprOpScenPart(itr.sp, itr.rp, itr.scen, part, chunk) +end +function eltype(::Type{PartitionDurationIterator{I}}) where {I<:StratReprOpScenario} + return StratReprOpScenPart +end + +function Base.show(io::IO, pd::StratReprOpScenPart) + return print(io, "sp$(pd.sp)-rp$(pd.rp)-sc$(pd.scen)-part$(pd.part)") +end +RepresentativeIndexable(::Type{<:StratReprOpScenPart}) = HasReprIndex() +ScenarioIndexable(::Type{<:StratReprOpScenPart}) = HasScenarioIndex() +_rper(pd::StratReprOpScenPart) = pd.rp +_opscen(pd::StratReprOpScenPart) = pd.scen + # Add function for generation of partitions from higher level function partition_duration(ts::TwoLevel, dur) return collect( @@ -73,3 +95,11 @@ function partition_duration( Iterators.flatten(partition_duration(osc, dur) for osc in opscenarios(ts)), ) end +function partition_duration( + ts::StratReprPeriod{T,RepresentativePeriod{T,OP}}, + dur, +) where {T,OP<:OperationalScenarios} + return collect( + Iterators.flatten(partition_duration(osc, dur) for osc in opscenarios(ts)), + ) +end diff --git a/test/runtests.jl b/test/runtests.jl index 3817c4b..2f98345 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1434,89 +1434,96 @@ end end @testitem "Profiles and partitions" begin - ts = TwoLevel(3, 5, SimpleTimes(6, 2)) - + # Declaration of the time profiles pp = PartitionProfile([3, 1, 2]) - vals = collect(pp[pd] for pd in partition_duration(ts, 4)) - @test vals == [3, 1, 2, 3, 1, 2, 3, 1, 2] - - sp1 = StrategicProfile([3, 1, 2]) - vals = collect(sp1[pd] for pd in partition_duration(ts, 4)) - @test vals == [3, 3, 3, 1, 1, 1, 2, 2, 2] - - sp2 = StrategicProfile([pp, pp+10, pp+20]) - vals = collect(sp2[pd] for pd in partition_duration(ts, 4)) - @test vals == [3, 1, 2, 13, 11, 12, 23, 21, 22] - - # Tests for time structure with operational scenarios - oscs = OperationalScenarios(2, SimpleTimes(6, 2)) - ts = TwoLevel(2, 1, oscs) - - vals = collect(pp[pd] for pd in partition_duration(ts, 4)) - @test vals == [3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2] + pp_vals = [3, 1, 2] scp1 = ScenarioProfile([3, 1]) - vals = collect(scp1[pd] for pd in partition_duration(ts, 4)) - @test vals == [3, 3, 3, 1, 1, 1, 3, 3, 3, 1, 1, 1] - + scp1_vals = [3, 3, 3, 1, 1, 1] scp2 = ScenarioProfile([pp, pp+10]) - vals = collect(scp2[pd] for pd in partition_duration(ts, 4)) - @test vals == [3, 1, 2, 13, 11, 12, 3, 1, 2, 13, 11, 12] - - sp1 = StrategicProfile([3, 1]) - vals = collect(sp1[pd] for pd in partition_duration(ts, 4)) - @test vals == [3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1] - - sp2 = StrategicProfile([3, 1]) - vals = collect(sp2[pd] for pd in partition_duration(ts, 4)) - @test vals == [3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1] - - sp3 = StrategicProfile([pp, pp+100]) - vals = collect(sp3[pd] for pd in partition_duration(ts, 4)) - @test vals == [3, 1, 2, 3, 1, 2, 103, 101, 102, 103, 101, 102] - - sp4 = StrategicProfile([scp1, scp1+100]) - vals = collect(sp4[pd] for pd in partition_duration(ts, 4)) - @test vals == [3, 3, 3, 1, 1, 1, 103, 103, 103, 101, 101, 101] - - sp5 = StrategicProfile([scp2, scp2+100]) - vals = collect(sp5[pd] for pd in partition_duration(ts, 4)) - @test vals == [3, 1, 2, 13, 11, 12, 103, 101, 102, 113, 111, 112] - - # Tests for time structure with representative periods - rps = RepresentativePeriods(2, 1, SimpleTimes(6, 2)) - ts = TwoLevel(2, 1, rps) - - vals = collect(pp[pd] for pd in partition_duration(ts, 4)) - @test vals == [3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2] + scp2_vals = [3, 1, 2, 13, 11, 12] rp1 = RepresentativeProfile([3, 1]) - vals = collect(rp1[pd] for pd in partition_duration(ts, 4)) - @test vals == [3, 3, 3, 1, 1, 1, 3, 3, 3, 1, 1, 1] - - rp2 = RepresentativeProfile([pp, pp+10]) - vals = collect(rp2[pd] for pd in partition_duration(ts, 4)) - @test vals == [3, 1, 2, 13, 11, 12, 3, 1, 2, 13, 11, 12] + rp1_vals(x::Int) = vcat(ones(Int64, x)*3, ones(Int64, x)) + rp2 = RepresentativeProfile([pp, pp+100]) + rp2_vals(x::Int) = vcat(repeat(pp_vals, x), repeat(pp_vals .+ 100, x)) + rp3 = RepresentativeProfile([scp1, scp1+100]) + rp3_vals = vcat(scp1_vals, scp1_vals .+ 100) + rp4 = RepresentativeProfile([scp2, scp2+100]) + rp4_vals = vcat(scp2_vals, scp2_vals .+ 100) sp1 = StrategicProfile([3, 1]) - vals = collect(sp1[pd] for pd in partition_duration(ts, 4)) - @test vals == [3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1] - - sp2 = StrategicProfile([3, 1]) - vals = collect(sp2[pd] for pd in partition_duration(ts, 4)) - @test vals == [3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1] - - sp3 = StrategicProfile([pp, pp+100]) - vals = collect(sp3[pd] for pd in partition_duration(ts, 4)) - @test vals == [3, 1, 2, 3, 1, 2, 103, 101, 102, 103, 101, 102] - - sp4 = StrategicProfile([rp1, rp1+100]) - vals = collect(sp4[pd] for pd in partition_duration(ts, 4)) - @test vals == [3, 3, 3, 1, 1, 1, 103, 103, 103, 101, 101, 101] + sp1_vals(x::Int) = vcat(ones(Int64, x)*3, ones(Int64, x)) + sp2 = StrategicProfile([pp, pp+1000]) + sp2_vals(x::Int) = vcat(repeat(pp_vals, x), repeat(pp_vals .+ 1000, x)) + sp3 = StrategicProfile([scp1, scp1+1000]) + sp3_vals(x::Int) = vcat(repeat(scp1_vals, x), repeat(scp1_vals .+ 1000, x)) + sp4 = StrategicProfile([scp2, scp2+1000]) + sp4_vals(x::Int) = vcat(repeat(scp2_vals, x), repeat(scp2_vals .+ 1000, x)) + sp5 = StrategicProfile([rp1, rp1+1000]) + sp5_vals(x::Int) = vcat(rp1_vals(x), rp1_vals(x) .+ 1000) + sp6 = StrategicProfile([rp2, rp2+1000]) + sp6_vals(x::Int) = vcat(rp2_vals(x), rp2_vals(x) .+ 1000) + sp7 = StrategicProfile([rp3, rp3+1000]) + sp8 = StrategicProfile([rp4, rp4+1000]) + + # Declaration of the core time time structures and utlities + st = SimpleTimes(6, 2) + oscs = OperationalScenarios(2, st) + rps1 = RepresentativePeriods(2, 1, st) + rps2 = RepresentativePeriods(2, 1, oscs) + pd_fun(ts) = partition_duration(ts, 4) + + # Tests for a TwoLevel time structure with operational scenarios + ts = TwoLevel(2, 1, st) + + @test collect(pp[pd] for pd in pd_fun(ts)) == repeat(pp_vals, 2) + @test collect(sp1[pd] for pd in pd_fun(ts)) == sp1_vals(3) + @test collect(sp2[pd] for pd in pd_fun(ts)) == sp2_vals(1) + + # Tests for a TwoLevel time structure with + # operational scenarios + ts = TwoLevel(2, 1, oscs) - sp5 = StrategicProfile([rp2, rp2+100]) - vals = collect(sp5[pd] for pd in partition_duration(ts, 4)) - @test vals == [3, 1, 2, 13, 11, 12, 103, 101, 102, 113, 111, 112] + @test collect(pp[pd] for pd in pd_fun(ts)) == repeat(pp_vals, 4) + @test collect(scp1[pd] for pd in pd_fun(ts)) == repeat(scp1_vals, 2) + @test collect(scp2[pd] for pd in pd_fun(ts)) == repeat(scp2_vals, 2) + @test collect(sp1[pd] for pd in pd_fun(ts)) == sp1_vals(6) + @test collect(sp2[pd] for pd in pd_fun(ts)) == sp2_vals(2) + @test collect(sp3[pd] for pd in pd_fun(ts)) == sp3_vals(1) + @test collect(sp4[pd] for pd in pd_fun(ts)) == sp4_vals(1) + + # Tests for a TwoLevel time structure with + # representative periods + ts = TwoLevel(2, 1, rps1) + + @test collect(pp[pd] for pd in pd_fun(ts)) == repeat(pp_vals, 4) + @test collect(rp1[pd] for pd in pd_fun(ts)) == repeat(rp1_vals(3), 2) + @test collect(rp2[pd] for pd in pd_fun(ts)) == repeat(rp2_vals(1), 2) + @test collect(sp1[pd] for pd in pd_fun(ts)) == sp1_vals(6) + @test collect(sp2[pd] for pd in pd_fun(ts)) == sp2_vals(2) + @test collect(sp5[pd] for pd in pd_fun(ts)) == sp5_vals(3) + @test collect(sp6[pd] for pd in pd_fun(ts)) == sp6_vals(1) + + # Tests for a TwoLevel time structure with + # representative periods and operational scenarios + ts = TwoLevel(2, 1, rps2) + + @test collect(pp[pd] for pd in pd_fun(ts)) == repeat(pp_vals, 8) + @test collect(scp1[pd] for pd in pd_fun(ts)) == repeat(scp1_vals, 4) + @test collect(scp2[pd] for pd in pd_fun(ts)) == repeat(scp2_vals, 4) + @test collect(rp1[pd] for pd in pd_fun(ts)) == repeat(rp1_vals(6), 2) + @test collect(rp2[pd] for pd in pd_fun(ts)) == repeat(rp2_vals(2), 2) + @test collect(rp3[pd] for pd in pd_fun(ts)) == repeat(rp3_vals, 2) + @test collect(rp4[pd] for pd in pd_fun(ts)) == repeat(rp4_vals, 2) + @test collect(sp1[pd] for pd in pd_fun(ts)) == sp1_vals(12) + @test collect(sp2[pd] for pd in pd_fun(ts)) == sp2_vals(4) + @test collect(sp3[pd] for pd in pd_fun(ts)) == sp3_vals(2) + @test collect(sp4[pd] for pd in pd_fun(ts)) == sp4_vals(2) + @test collect(sp5[pd] for pd in pd_fun(ts)) == sp5_vals(6) + @test collect(sp6[pd] for pd in pd_fun(ts)) == sp6_vals(2) + @test collect(sp7[pd] for pd in pd_fun(ts)) == vcat(rp3_vals, rp3_vals .+ 1000) + @test collect(sp8[pd] for pd in pd_fun(ts)) == vcat(rp4_vals, rp4_vals .+ 1000) end @testitem "Profile conversion" begin @@ -1880,7 +1887,7 @@ end @test (sum(duration(t) for t in s) >= 5) || (last(periods) in s) end - # Test of the iterator utilities when running on a strategic periods + # Test of the duration partitions when running on a strategic periods ts_ops = SimpleTimes([1, 2, 3, 6, 1, 1, 1, 1, 1, 1]) ts = TwoLevel(2, 1, ts_ops) ops = collect(ts) @@ -1907,8 +1914,8 @@ end @test length(pds_tl) == 6 @test all(pds_sp[k] == pds_tl[k] for k in 1:3) - # Test of the iterator utilities when running on a strategic periods with operational - # scenarios + # Test of the duration partitions when running on a strategic periods with + # operational scenarios ts_ops = SimpleTimes([1, 2, 3, 6, 1, 1, 1, 1, 1, 1]) oscs = OperationalScenarios(2, ts_ops) ts = TwoLevel(2, 1, oscs) @@ -1916,21 +1923,21 @@ end sp = first(strategic_periods(ts)) osc = first(opscenarios(sp)) - psc_osc = [pd for pd in partition_duration(osc, 6)] + pds_osc = [pd for pd in partition_duration(osc, 6)] idx = [1:3, 4:4, 5:10] - @test typeof(psc_osc[1]) <: TimeStruct.StratOpScenPart{3,<:TimeStruct.OperationalPeriod} - @test all(collect(psc_osc[k]) == ops[l] for (k, l) in enumerate(idx)) - @test first(psc_osc[1]) == first(ops) - @test last(psc_osc[1]) == ops[3] - @test length(psc_osc[1]) == 3 - @test repr(psc_osc[3]) == "sp1-sc1-part3" + @test typeof(pds_osc[1]) <: TimeStruct.StratOpScenPart{3,<:TimeStruct.OperationalPeriod} + @test all(collect(pds_osc[k]) == ops[l] for (k, l) in enumerate(idx)) + @test first(pds_osc[1]) == first(ops) + @test last(pds_osc[1]) == ops[3] + @test length(pds_osc[1]) == 3 + @test repr(pds_osc[3]) == "sp1-sc1-part3" - @test length(psc_osc) == 3 + @test length(pds_osc) == 3 @test typeof(partition_duration(osc, 6)) <: TimeStruct.PartitionDurationIterator{<:TimeStruct.StratOpScenario} @test eltype(partition_duration(osc, 6)) == TimeStruct.StratOpScenPart - @test all(sum(duration(t) for t in pd) >= 6 for pd in psc_osc) + @test all(sum(duration(t) for t in pd) >= 6 for pd in pds_osc) # Test of the iterator invariants pds_tl = partition_duration(ts, 6) @@ -1938,10 +1945,10 @@ end @test length(pds_tl) == 12 @test length(pds_sp) == 6 @test all(pds_sp[k] == pds_tl[k] for k in 1:3) - @test all(psc_osc[k] == pds_tl[k] for k in 1:3) + @test all(pds_osc[k] == pds_tl[k] for k in 1:3) - # Test of the iterator utilities when running on a strategic periods with representative - # periods + # Test of the duration partitions when running on a strategic periods with + # representative periods ts_ops = SimpleTimes([1, 2, 3, 6, 1, 1, 1, 1, 1, 1]) rps = RepresentativePeriods(2, 1, ts_ops) ts = TwoLevel(2, 1, rps) @@ -1972,6 +1979,45 @@ end @test length(pds_sp) == 6 @test all(pds_sp[k] == pds_tl[k] for k in 1:3) @test all(pds_rp[k] == pds_tl[k] for k in 1:3) + + # Test of the duration partitions when running on a strategic periods with + # operational scenarios and representative periods + ts_ops = SimpleTimes([1, 2, 3, 6, 1, 1, 1, 1, 1, 1]) + oscs = OperationalScenarios(2, ts_ops) + rps = RepresentativePeriods(2, 1, oscs) + ts = TwoLevel(2, 1, rps) + ops = collect(ts) + + sp = first(strategic_periods(ts)) + rp = first(repr_periods(sp)) + osc = first(opscenarios(sp)) + pds_osc = [pd for pd in partition_duration(osc, 6)] + idx = [1:3, 4:4, 5:10] + + @test typeof(pds_osc[1]) <: + TimeStruct.StratReprOpScenPart{3,<:TimeStruct.OperationalPeriod} + @test all(collect(pds_osc[k]) == ops[l] for (k, l) in enumerate(idx)) + @test first(pds_osc[1]) == first(ops) + @test last(pds_osc[1]) == ops[3] + @test length(pds_osc[1]) == 3 + @test repr(pds_osc[3]) == "sp1-rp1-sc1-part3" + + @test length(pds_osc) == 3 + @test typeof(partition_duration(osc, 6)) <: + TimeStruct.PartitionDurationIterator{<:TimeStruct.StratReprOpScenario} + @test eltype(partition_duration(osc, 6)) == TimeStruct.StratReprOpScenPart + @test all(sum(duration(t) for t in pd) >= 6 for pd in pds_osc) + + # Test of the iterator invariants + pds_tl = partition_duration(ts, 6) + pds_sp = [pd for pd in partition_duration(sp, 6)] + pds_rp = [pd for pd in partition_duration(rp, 6)] + @test length(pds_tl) == 24 + @test length(pds_sp) == 12 + @test length(pds_rp) == 6 + @test all(pds_sp[k] == pds_tl[k] for k in 1:3) + @test all(pds_rp[k] == pds_tl[k] for k in 1:3) + @test all(pds_osc[k] == pds_tl[k] for k in 1:3) end @testitem "Indexing of operational structures" begin From 3b9d3517e07c0cb65f771eed3d6d5037bde98c33 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Thu, 11 Jun 2026 15:53:55 +0200 Subject: [PATCH 8/8] Added support for * OperationalScenarios * RepresentativePeriods * RepresentativePeriods{OperationalScenarios} --- src/partitions/opscenarios.jl | 18 +++++ src/partitions/rep_periods.jl | 43 ++++++++++++ src/partitions/strat_periods.jl | 2 + test/runtests.jl | 115 ++++++++++++++++++++++++++++++-- 4 files changed, 173 insertions(+), 5 deletions(-) diff --git a/src/partitions/opscenarios.jl b/src/partitions/opscenarios.jl index 3fb5ae7..62c6cf1 100644 --- a/src/partitions/opscenarios.jl +++ b/src/partitions/opscenarios.jl @@ -3,3 +3,21 @@ abstract type AbstractOpScenPart{T} <: PartitionDuration{T} end ScenarioIndexable(::Type{<:AbstractOpScenPart}) = HasScenarioIndex() _opscen(pd::AbstractOpScenPart) = pd.scen + +# Add partition type with constructor for indexing when operational scenarios are present +struct OpScenPart{N,T} <: AbstractOpScenPart{T} + scen::Int + part::Int + chunk::NTuple{N,T} +end +PartitionDuration(itr::OperationalScenario, part, chunk) = OpScenPart(itr.scen, part, chunk) +eltype(::Type{PartitionDurationIterator{I}}) where {I<:OperationalScenario} = OpScenPart + +Base.show(io::IO, pd::OpScenPart) = print(io, "sc$(pd.scen)-part$(pd.part)") + +# Add function for generation of partitions from higher level +function partition_duration(ts::OperationalScenarios, dur) + return collect( + Iterators.flatten(partition_duration(osc, dur) for osc in opscenarios(ts)), + ) +end diff --git a/src/partitions/rep_periods.jl b/src/partitions/rep_periods.jl index 064b1cb..ad574b4 100644 --- a/src/partitions/rep_periods.jl +++ b/src/partitions/rep_periods.jl @@ -3,3 +3,46 @@ abstract type AbstractReprPart{T} <: PartitionDuration{T} end RepresentativeIndexable(::Type{<:AbstractReprPart}) = HasReprIndex() _rper(pd::AbstractReprPart) = pd.rp + +# Add partition type with constructor for indexing when representative periods are present +struct ReprPart{N,T} <: AbstractReprPart{T} + rp::Int + part::Int + chunk::NTuple{N,T} +end +PartitionDuration(itr::RepresentativePeriod, part, chunk) = ReprPart(itr.rp, part, chunk) +eltype(::Type{PartitionDurationIterator{I}}) where {I<:RepresentativePeriod} = ReprPart + +Base.show(io::IO, pd::ReprPart) = print(io, "rp$(pd.rp)-part$(pd.part)") + +# Add partition type with constructor for indexing when representative periods and operational +# scenarios are present +struct ReprOpScenPart{N,T} <: AbstractReprPart{T} + rp::Int + scen::Int + part::Int + chunk::NTuple{N,T} +end +function PartitionDuration(itr::ReprOpScenario, part, chunk) + return ReprOpScenPart(itr.rp, itr.scen, part, chunk) +end +eltype(::Type{PartitionDurationIterator{I}}) where {I<:ReprOpScenario} = ReprOpScenPart + +Base.show(io::IO, pd::ReprOpScenPart) = print(io, "rp$(pd.rp)-sc$(pd.scen)-part$(pd.part)") +ScenarioIndexable(::Type{<:ReprOpScenPart}) = HasScenarioIndex() +_opscen(pd::ReprOpScenPart) = pd.scen + +# Add function for generation of partitions from higher level +function partition_duration(ts::RepresentativePeriods, dur) + return collect( + Iterators.flatten(partition_duration(rp, dur) for rp in repr_periods(ts)), + ) +end +function partition_duration( + ts::RepresentativePeriod{T,OP}, + dur, +) where {T,OP<:OperationalScenarios} + return collect( + Iterators.flatten(partition_duration(osc, dur) for osc in opscenarios(ts)), + ) +end diff --git a/src/partitions/strat_periods.jl b/src/partitions/strat_periods.jl index 12c085f..3235b93 100644 --- a/src/partitions/strat_periods.jl +++ b/src/partitions/strat_periods.jl @@ -51,6 +51,8 @@ end ScenarioIndexable(::Type{<:StratOpScenPart}) = HasScenarioIndex() _opscen(pd::StratOpScenPart) = pd.scen +# Add partition type with constructor for indexing when strategic periods, representative +# periods and operational scenarios are present struct StratReprOpScenPart{N,T} <: AbstractStratPart{T} sp::Int rp::Int diff --git a/test/runtests.jl b/test/runtests.jl index 2f98345..97c757c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1474,6 +1474,25 @@ end rps2 = RepresentativePeriods(2, 1, oscs) pd_fun(ts) = partition_duration(ts, 4) + # Tests for an operational scenarios time structure + @test collect(pp[pd] for pd in partition_duration(oscs, 4)) == repeat(pp_vals, 2) + @test collect(scp1[pd] for pd in partition_duration(oscs, 4)) == scp1_vals + @test collect(scp2[pd] for pd in partition_duration(oscs, 4)) == scp2_vals + + # Tests for an representative periods time structure + @test collect(pp[pd] for pd in partition_duration(rps1, 4)) == repeat(pp_vals, 2) + @test collect(rp1[pd] for pd in partition_duration(rps1, 4)) == rp1_vals(3) + @test collect(rp2[pd] for pd in partition_duration(rps1, 4)) == rp2_vals(1) + + # Tests for an representative periods time structure with operational scenarios + @test collect(pp[pd] for pd in partition_duration(rps2, 4)) == repeat(pp_vals, 4) + @test collect(scp1[pd] for pd in partition_duration(rps2, 4)) == repeat(scp1_vals, 2) + @test collect(scp2[pd] for pd in partition_duration(rps2, 4)) == repeat(scp2_vals, 2) + @test collect(rp1[pd] for pd in partition_duration(rps2, 4)) == rp1_vals(6) + @test collect(rp2[pd] for pd in partition_duration(rps2, 4)) == rp2_vals(2) + @test collect(rp3[pd] for pd in partition_duration(rps2, 4)) == rp3_vals + @test collect(rp4[pd] for pd in partition_duration(rps2, 4)) == rp4_vals + # Tests for a TwoLevel time structure with operational scenarios ts = TwoLevel(2, 1, st) @@ -1887,7 +1906,93 @@ end @test (sum(duration(t) for t in s) >= 5) || (last(periods) in s) end - # Test of the duration partitions when running on a strategic periods + # Test of the duration partitions when using operational scenarios + ts_ops = SimpleTimes([1, 2, 3, 6, 1, 1, 1, 1, 1, 1]) + ts = OperationalScenarios(2, ts_ops) + ops = collect(ts) + + osc = first(opscenarios(ts)) + pds_osc = [pd for pd in partition_duration(osc, 6)] + idx = [1:3, 4:4, 5:10] + + @test typeof(pds_osc[1]) <: TimeStruct.OpScenPart{3,<:TimeStruct.ScenarioPeriod} + @test all(collect(pds_osc[k]) == ops[l] for (k, l) in enumerate(idx)) + @test first(pds_osc[1]) == first(ops) + @test last(pds_osc[1]) == ops[3] + @test length(pds_osc[1]) == 3 + @test repr(pds_osc[3]) == "sc1-part3" + + @test length(pds_osc) == 3 + @test typeof(partition_duration(osc, 6)) <: + TimeStruct.PartitionDurationIterator{<:TimeStruct.OperationalScenario} + @test eltype(partition_duration(osc, 6)) == TimeStruct.OpScenPart + @test all(sum(duration(t) for t in pd) >= 6 for pd in pds_osc) + + # Test of the iterator invariants + pds_tl = partition_duration(ts, 6) + @test length(pds_tl) == 6 + @test all(pds_osc[k] == pds_tl[k] for k in 1:3) + + # Test of the duration partitions when using representative periods + ts_ops = SimpleTimes([1, 2, 3, 6, 1, 1, 1, 1, 1, 1]) + ts = RepresentativePeriods(2, 1, ts_ops) + ops = collect(ts) + + rp = first(repr_periods(ts)) + pds_rp = [pd for pd in partition_duration(rp, 6)] + idx = [1:3, 4:4, 5:10] + + @test typeof(pds_rp[1]) <: TimeStruct.ReprPart{3,<:TimeStruct.ReprPeriod} + @test all(collect(pds_rp[k]) == ops[l] for (k, l) in enumerate(idx)) + @test first(pds_rp[1]) == first(ops) + @test last(pds_rp[1]) == ops[3] + @test length(pds_rp[1]) == 3 + @test repr(pds_rp[3]) == "rp1-part3" + + @test length(pds_rp) == 3 + @test typeof(partition_duration(rp, 6)) <: + TimeStruct.PartitionDurationIterator{<:TimeStruct.RepresentativePeriod} + @test eltype(partition_duration(rp, 6)) == TimeStruct.ReprPart + @test all(sum(duration(t) for t in pd) >= 6 for pd in pds_rp) + + # Test of the iterator invariants + pds_tl = partition_duration(ts, 6) + @test length(pds_tl) == 6 + @test all(pds_rp[k] == pds_tl[k] for k in 1:3) + + # Test of the duration partitions when using representative periods + # with operational scenarios + ts_ops = SimpleTimes([1, 2, 3, 6, 1, 1, 1, 1, 1, 1]) + oscs = OperationalScenarios(2, ts_ops) + ts = RepresentativePeriods(2, 1, oscs) + ops = collect(ts) + + rp = first(repr_periods(ts)) + osc = first(opscenarios(rp)) + pds_osc = [pd for pd in partition_duration(osc, 6)] + idx = [1:3, 4:4, 5:10] + + @test typeof(pds_osc[1]) <: TimeStruct.ReprOpScenPart{3,<:TimeStruct.ReprPeriod} + @test all(collect(pds_osc[k]) == ops[l] for (k, l) in enumerate(idx)) + @test first(pds_osc[1]) == first(ops) + @test last(pds_osc[1]) == ops[3] + @test length(pds_osc[1]) == 3 + @test repr(pds_osc[3]) == "rp1-sc1-part3" + + @test length(pds_osc) == 3 + @test typeof(partition_duration(osc, 6)) <: + TimeStruct.PartitionDurationIterator{<:TimeStruct.ReprOpScenario} + @test eltype(partition_duration(osc, 6)) == TimeStruct.ReprOpScenPart + @test all(sum(duration(t) for t in pd) >= 6 for pd in pds_osc) + + # Test of the iterator invariants + pds_tl = partition_duration(ts, 6) + pds_rp = [pd for pd in partition_duration(rp, 6)] + @test length(pds_tl) == 12 + @test all(pds_rp[k] == pds_tl[k] for k in 1:3) + @test all(pds_osc[k] == pds_tl[k] for k in 1:3) + + # Test of the duration partitions when using strategic periods ts_ops = SimpleTimes([1, 2, 3, 6, 1, 1, 1, 1, 1, 1]) ts = TwoLevel(2, 1, ts_ops) ops = collect(ts) @@ -1914,7 +2019,7 @@ end @test length(pds_tl) == 6 @test all(pds_sp[k] == pds_tl[k] for k in 1:3) - # Test of the duration partitions when running on a strategic periods with + # Test of the duration partitions when using strategic periods with # operational scenarios ts_ops = SimpleTimes([1, 2, 3, 6, 1, 1, 1, 1, 1, 1]) oscs = OperationalScenarios(2, ts_ops) @@ -1947,7 +2052,7 @@ end @test all(pds_sp[k] == pds_tl[k] for k in 1:3) @test all(pds_osc[k] == pds_tl[k] for k in 1:3) - # Test of the duration partitions when running on a strategic periods with + # Test of the duration partitions when using strategic periods with # representative periods ts_ops = SimpleTimes([1, 2, 3, 6, 1, 1, 1, 1, 1, 1]) rps = RepresentativePeriods(2, 1, ts_ops) @@ -1980,7 +2085,7 @@ end @test all(pds_sp[k] == pds_tl[k] for k in 1:3) @test all(pds_rp[k] == pds_tl[k] for k in 1:3) - # Test of the duration partitions when running on a strategic periods with + # Test of the duration partitions when using strategic periods with # operational scenarios and representative periods ts_ops = SimpleTimes([1, 2, 3, 6, 1, 1, 1, 1, 1, 1]) oscs = OperationalScenarios(2, ts_ops) @@ -1990,7 +2095,7 @@ end sp = first(strategic_periods(ts)) rp = first(repr_periods(sp)) - osc = first(opscenarios(sp)) + osc = first(opscenarios(rp)) pds_osc = [pd for pd in partition_duration(osc, 6)] idx = [1:3, 4:4, 5:10]