diff --git a/docs/src/reference/api.md b/docs/src/reference/api.md index fac9347..c8527ec 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) @@ -78,6 +79,7 @@ end_oper_time ```@docs FixedProfile OperationalProfile +PartitionProfile RepresentativeProfile ScenarioProfile StrategicProfile diff --git a/src/TimeStruct.jl b/src/TimeStruct.jl index 857a2d3..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 @@ -40,6 +44,7 @@ export TreeNode export TimeProfile export FixedProfile export OperationalProfile +export PartitionProfile export ScenarioProfile export StrategicProfile export StrategicStochasticProfile @@ -49,7 +54,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/partitions/opscenarios.jl b/src/partitions/opscenarios.jl new file mode 100644 index 0000000..62c6cf1 --- /dev/null +++ b/src/partitions/opscenarios.jl @@ -0,0 +1,23 @@ +# Add generic partition duration type +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 new file mode 100644 index 0000000..ad574b4 --- /dev/null +++ b/src/partitions/rep_periods.jl @@ -0,0 +1,48 @@ +# Add generic partition duration type +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 new file mode 100644 index 0000000..3235b93 --- /dev/null +++ b/src/partitions/strat_periods.jl @@ -0,0 +1,107 @@ +# 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 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 + 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( + 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 +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/src/profiles.jl b/src/profiles.jl index f67d575..ee94740 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 @@ -202,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 @@ -261,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 @@ -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,19 +518,35 @@ 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 +(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 +(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) 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 b47df38..f536d6e 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -172,6 +172,74 @@ 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) + +_part(pd::PartitionDuration) = pd.part + +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 +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 + """ end_oper_time(t, ts) diff --git a/test/runtests.jl b/test/runtests.jl index d2b2319..97c757c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1433,6 +1433,118 @@ end @test vals == [1, 2, 3, 4, 2, 4, 6, 8, 3, 6, 9, 12] end +@testitem "Profiles and partitions" begin + # Declaration of the time profiles + pp = PartitionProfile([3, 1, 2]) + pp_vals = [3, 1, 2] + + scp1 = ScenarioProfile([3, 1]) + scp1_vals = [3, 3, 3, 1, 1, 1] + scp2 = ScenarioProfile([pp, pp+10]) + scp2_vals = [3, 1, 2, 13, 11, 12] + + rp1 = RepresentativeProfile([3, 1]) + 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]) + 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 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) + + @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) + + @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 fp = FixedProfile(12) @test TimeStruct._internal_convert(Int64, fp) == FixedProfile(12) @@ -1498,6 +1610,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,17 +1667,22 @@ 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, 5, 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) 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 +1713,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 +1729,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) @@ -1609,6 +1745,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) @@ -1621,6 +1761,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) @@ -1761,6 +1905,224 @@ end for s in sdur @test (sum(duration(t) for t in s) >= 5) || (last(periods) in s) end + + # 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) + + 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) + + # 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) + ts = TwoLevel(2, 1, oscs) + ops = collect(ts) + + sp = first(strategic_periods(ts)) + 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.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(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 pds_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(pds_osc[k] == pds_tl[k] for k in 1:3) + + # 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) + 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) + + # 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) + 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(rp)) + 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