From e9378383e5e986c3328325f008e3d5b89ed1e203 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Tue, 23 Jun 2026 11:53:55 +0200 Subject: [PATCH 1/2] Added PartitionDuration for TwoLevelTree --- src/TimeStruct.jl | 1 + src/partitions/tree_periods.jl | 134 +++++++++++++++++++++++ src/profiles.jl | 8 +- test/runtests.jl | 192 ++++++++++++++++++++++++++++++++- 4 files changed, 332 insertions(+), 3 deletions(-) create mode 100644 src/partitions/tree_periods.jl diff --git a/src/TimeStruct.jl b/src/TimeStruct.jl index e2b9195..4436783 100644 --- a/src/TimeStruct.jl +++ b/src/TimeStruct.jl @@ -28,6 +28,7 @@ include("profiles.jl") include("utils.jl") include("partitions/strat_periods.jl") +include("partitions/tree_periods.jl") include("partitions/rep_periods.jl") include("partitions/opscenarios.jl") diff --git a/src/partitions/tree_periods.jl b/src/partitions/tree_periods.jl new file mode 100644 index 0000000..815cbff --- /dev/null +++ b/src/partitions/tree_periods.jl @@ -0,0 +1,134 @@ +# Add generic partition duration type +abstract type AbstractTreePart{T} <: AbstractStratPart{T} end + +StrategicTreeIndexable(::Type{<:AbstractTreePart}) = HasStratTreeIndex() +_branch(pd::AbstractTreePart) = pd.branch + +# Add partition type with constructor for indexing when strategic periods are present +struct StratNodePart{N,T} <: AbstractTreePart{T} + sp::Int + branch::Int + part::Int + chunk::NTuple{N,T} +end +function PeriodPartition(itr::StratNode, part, chunk) + return StratNodePart(_strat_per(itr), _branch(itr), part, chunk) +end +eltype(::Type{PartitionDurationIterator{I,T,D}}) where {I<:StratNode,T,D} = StratNodePart + +function Base.show(io::IO, pd::StratNodePart) + return print(io, "sp$(_strat_per(pd))-br$(_branch(pd))-part$(_part(pd))") +end + +# Add partition type with constructor for indexing when strategic and representative periods +# are present +struct StratNodeReprPart{N,T} <: AbstractTreePart{T} + sp::Int + branch::Int + rp::Int + part::Int + chunk::NTuple{N,T} +end +function PeriodPartition(itr::StratNodeReprPeriod, part, chunk) + return StratNodeReprPart(_strat_per(itr), _branch(itr), _rper(itr), part, chunk) +end +function eltype(::Type{PartitionDurationIterator{I,T,D}}) where {I<:StratNodeReprPeriod,T,D} + return StratNodeReprPart +end + +RepresentativeIndexable(::Type{<:StratNodeReprPart}) = HasReprIndex() +_rper(pd::StratNodeReprPart) = pd.rp + +function Base.show(io::IO, pd::StratNodeReprPart) + return print(io, "sp$(_strat_per(pd))-br$(_branch(pd))-rp$(_rper(pd))-part$(_part(pd))") +end + +# Add partition type with constructor for indexing when strategic periods and operational +# scenarios are present +struct StratNodeOpScenPart{N,T} <: AbstractTreePart{T} + sp::Int + branch::Int + scen::Int + part::Int + chunk::NTuple{N,T} +end +function PeriodPartition(itr::StratNodeOpScenario, part, chunk) + return StratNodeOpScenPart(_strat_per(itr), _branch(itr), _opscen(itr), part, chunk) +end +function eltype(::Type{PartitionDurationIterator{I,T,D}}) where {I<:StratNodeOpScenario,T,D} + return StratNodeOpScenPart +end + +function Base.show(io::IO, pd::StratNodeOpScenPart) + return print( + io, + "sp$(_strat_per(pd))-br$(_branch(pd))-sc$(_opscen(pd))-part$(_part(pd))", + ) +end +ScenarioIndexable(::Type{<:StratNodeOpScenPart}) = HasScenarioIndex() +_opscen(pd::StratNodeOpScenPart) = pd.scen + +# Add partition type with constructor for indexing when strategic periods, representative +# periods and operational scenarios are present +struct StratNodeReprOpScenPart{N,T} <: AbstractTreePart{T} + sp::Int + branch::Int + rp::Int + scen::Int + part::Int + chunk::NTuple{N,T} +end +function PeriodPartition(itr::StratNodeReprOpScenario, part, chunk) + return StratNodeReprOpScenPart( + _strat_per(itr), + _branch(itr), + _rper(itr), + _opscen(itr), + part, + chunk, + ) +end +function eltype( + ::Type{PartitionDurationIterator{I,T,D}}, +) where {I<:StratNodeReprOpScenario,T,D} + return StratNodeReprOpScenPart +end + +function Base.show(io::IO, pd::StratNodeReprOpScenPart) + return print( + io, + "sp$(_strat_per(pd))-br$(_branch(pd))-rp$(_rper(pd))-sc$(_opscen(pd))-part$(_part(pd))", + ) +end +RepresentativeIndexable(::Type{<:StratNodeReprOpScenPart}) = HasReprIndex() +ScenarioIndexable(::Type{<:StratNodeReprOpScenPart}) = HasScenarioIndex() +_rper(pd::StratNodeReprOpScenPart) = pd.rp +_opscen(pd::StratNodeReprOpScenPart) = pd.scen + +# Add function for generation of partitions from higher level +function partition_duration(ts::TwoLevelTree, dur) + return collect( + Iterators.flatten(partition_duration(sp, dur) for sp in strategic_periods(ts)), + ) +end +function partition_duration( + ts::StratNode{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::StratNode{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::StratNodeReprPeriod{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 1c24f9b..b20232b 100644 --- a/src/profiles.jl +++ b/src/profiles.jl @@ -375,7 +375,7 @@ end function Base.getindex( ssp::StrategicStochasticProfile, period::T, -) where {T<:Union{TimePeriod,TimeStructurePeriod}} +) where {T<:Union{TimePeriod,TimeStructurePeriod,PeriodPartition}} return _value_lookup(StrategicTreeIndexable(T), ssp, period) end @@ -384,7 +384,7 @@ Base.getindex(TP::TimeProfile, inds...) = [TP[i] for i in inds] function Base.getindex( TP::TimeProfile, inds::Vector{T}, -) where {T<:Union{TimePeriod,TimeStructurePeriod}} +) where {T<:Union{TimePeriod,TimeStructurePeriod,PeriodPartition}} return [TP[i] for i in inds] end function Base.getindex( @@ -563,6 +563,10 @@ function +(a::StrategicStochasticProfile{T}, b::OperationalProfile{S}) where {T, return StrategicStochasticProfile([[p + b for p in v] for v in a.vals]) end +(a::OperationalProfile{T}, b::StrategicStochasticProfile{S}) where {T,S} = b + a +function +(a::StrategicStochasticProfile{T}, b::PartitionProfile{S}) where {T,S} + return StrategicStochasticProfile([[p + b for p in v] for v in a.vals]) +end ++(a::PartitionProfile{T}, b::StrategicStochasticProfile{S}) where {T,S} = b + a function +(a::FixedProfile{T}, b::StrategicStochasticProfile{S}) where {T,S} return StrategicStochasticProfile([[a + p for p in v] for v in b.vals]) end diff --git a/test/runtests.jl b/test/runtests.jl index 156f3fa..21e1f2d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1467,6 +1467,12 @@ end sp7 = StrategicProfile([rp3, rp3+1000]) sp8 = StrategicProfile([rp4, rp4+1000]) + sn1 = StrategicStochasticProfile([[1], [21, 22]]) + sn1_vals(x::Int) = vcat(ones(Int, x)*1, ones(Int, x)*21, ones(Int, x)*22) + sn2 = StrategicStochasticProfile([[pp], [pp+1000, pp+1500]]) + sn2_vals(x::Int) = + vcat(repeat(pp_vals, x), repeat(pp_vals .+ 1000, x), repeat(pp_vals .+ 1500, x)) + # Declaration of the core time structures and utilities st = SimpleTimes(6, 2) oscs = OperationalScenarios(2, st) @@ -1493,7 +1499,7 @@ end @test collect(rp3[pd] for pd in pd_fun(rps2)) == rp3_vals @test collect(rp4[pd] for pd in pd_fun(rps2)) == rp4_vals - # Tests for a TwoLevel time structure with operational scenarios + # Tests for a TwoLevel time structure ts = TwoLevel(2, 1, st) @test collect(pp[pd] for pd in pd_fun(ts)) == repeat(pp_vals, 2) @@ -1543,6 +1549,14 @@ end @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) + + # Tests for a TwoLevel tree time structure with + # representative periods and operational scenarios + ts = TwoLevelTree(1, [2], st) + + @test collect(pp[pd] for pd in pd_fun(ts)) == repeat(pp_vals, 3) + @test collect(sn1[pd] for pd in pd_fun(ts)) == sn1_vals(3) + @test collect(sn2[pd] for pd in pd_fun(ts)) == sn2_vals(1) end @testitem "Profile conversion" begin @@ -1782,6 +1796,7 @@ end # StrategicStochasticProfile setup tree = TwoLevelTree(5, [3, 2], SimpleTimes(5, 1)) + tree_part = partition_duration(tree, 2) ssp = StrategicStochasticProfile([ [OperationalProfile([1, 2, 3, 4, 5])], [ @@ -1833,6 +1848,10 @@ end @test all((ssp+op)[t] == ssp[t] + op[t] for t in tree) @test all((ssp+op)[t] == (op+ssp)[t] for t in tree) + # StrategicStochasticProfile + PartitionProfile + @test all((ssp2+pp)[t] == ssp2[t] + pp[t] for t in tree_part) + @test all((ssp2+pp)[t] == (pp+ssp2)[t] for t in tree_part) + # FixedProfile + StrategicStochasticProfile @test all((fp+ssp)[t] == fp[t] + ssp[t] for t in tree) @test all((fp+ssp)[t] == (ssp+fp)[t] for t in tree) @@ -2272,6 +2291,177 @@ end @test length(pds_sp) == 3 @test all(pds_sp[k] == pds_ts[k] for k in 1:3) end + + # Test of the duration partitions when using strategic nodes + @testset "Partitions - TwoLevelTree" begin + ts = TwoLevelTree(1, [2], ts_ops) + ops = collect(ts) + + sp = strategic_periods(ts)[2] + pds_sp = [pd for pd in partition_duration(sp, 6)] + idx = [11:13, 14:14, 15:20] + + @test typeof(pds_sp[1]) <: TimeStruct.StratNodePart{3,<:TimeStruct.TreePeriod} + @test all(collect(pds_sp[k]) == ops[l] for (k, l) in enumerate(idx)) + @test first(pds_sp[1]) == ops[11] + @test last(pds_sp[1]) == ops[13] + @test length(pds_sp[1]) == 3 + @test repr(pds_sp[3]) == "sp2-br1-part3" + + @test length(pds_sp) == 3 + @test isa( + partition_duration(sp, 6), + TimeStruct.PartitionDurationIterator{ + TimeStruct.StratNode{Int,Int,SimpleTimes{Int}}, + Int, + FixedProfile{Int}, + }, + ) + @test eltype(partition_duration(sp, 6)) == TimeStruct.StratNodePart + @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) == 9 + @test all(pds_sp[k] == pds_tl[k+3] for k in 1:3) + end + + # Test of the duration partitions when using strategic nodes with + # operational scenarios + @testset "Partitions - TwoLevelTree{OperationalScenarios}" begin + oscs = OperationalScenarios(2, ts_ops) + ts = TwoLevelTree(1, [2], oscs) + ops = collect(ts) + + sp = strategic_periods(ts)[2] + osc = first(opscenarios(sp)) + pds_osc = [pd for pd in partition_duration(osc, 6)] + idx = [21:23, 24:24, 25:30] + + @test typeof(pds_osc[1]) <: + TimeStruct.StratNodeOpScenPart{3,<:TimeStruct.TreePeriod} + @test all(collect(pds_osc[k]) == ops[l] for (k, l) in enumerate(idx)) + @test first(pds_osc[1]) == ops[21] + @test last(pds_osc[1]) == ops[23] + @test length(pds_osc[1]) == 3 + @test repr(pds_osc[3]) == "sp2-br1-sc1-part3" + + @test length(pds_osc) == 3 + @test isa( + partition_duration(osc, 6), + TimeStruct.PartitionDurationIterator{ + TimeStruct.StratNodeOpScenario{ + Int, + TimeStruct.OperationalScenario{Int,SimpleTimes{Int}}, + }, + Int, + FixedProfile{Int}, + }, + ) + @test eltype(partition_duration(osc, 6)) == TimeStruct.StratNodeOpScenPart + @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) == 18 + @test length(pds_sp) == 6 + @test all(pds_sp[k] == pds_tl[k+6] for k in 1:3) + @test all(pds_osc[k] == pds_tl[k+6] for k in 1:3) + end + + # Test of the duration partitions when using strategic nodes with + # representative periods + @testset "Partitions - TwoLevelTree{RepresentativePeriods}" begin + rps = RepresentativePeriods(2, 1, ts_ops) + ts = TwoLevelTree(1, [2], rps) + ops = collect(ts) + + sp = strategic_periods(ts)[2] + rp = first(repr_periods(sp)) + pds_rp = [pd for pd in partition_duration(rp, 6)] + idx = [21:23, 24:24, 25:30] + + @test typeof(pds_rp[1]) <: + TimeStruct.StratNodeReprPart{3,<:TimeStruct.TreePeriod} + @test all(collect(pds_rp[k]) == ops[l] for (k, l) in enumerate(idx)) + @test first(pds_rp[1]) == ops[21] + @test last(pds_rp[1]) == ops[23] + @test length(pds_rp[1]) == 3 + @test repr(pds_rp[3]) == "sp2-br1-rp1-part3" + + @test length(pds_rp) == 3 + @test isa( + partition_duration(rp, 6), + TimeStruct.PartitionDurationIterator{ + TimeStruct.StratNodeReprPeriod{ + Int, + TimeStruct.RepresentativePeriod{Int,SimpleTimes{Int}}, + }, + Int, + FixedProfile{Int}, + }, + ) + @test eltype(partition_duration(rp, 6)) == TimeStruct.StratNodeReprPart + @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) == 18 + @test length(pds_sp) == 6 + @test all(pds_sp[k] == pds_tl[k+6] for k in 1:3) + @test all(pds_rp[k] == pds_tl[k+6] for k in 1:3) + end + + # Test of the duration partitions when using strategic nodes with + # operational scenarios and representative periods + @testset "Partitions - TwoLevel{RepresentativePeriods{OperationalScenarios}}" begin + oscs = OperationalScenarios(2, ts_ops) + rps = RepresentativePeriods(2, 1, oscs) + ts = TwoLevelTree(1, [2], rps) + ops = collect(ts) + + sp = strategic_periods(ts)[2] + rp = first(repr_periods(sp)) + osc = first(opscenarios(rp)) + pds_osc = [pd for pd in partition_duration(osc, 6)] + idx = [41:43, 44:44, 45:50] + + @test typeof(pds_osc[1]) <: + TimeStruct.StratNodeReprOpScenPart{3,<:TimeStruct.TreePeriod} + @test all(collect(pds_osc[k]) == ops[l] for (k, l) in enumerate(idx)) + @test first(pds_osc[1]) == ops[41] + @test last(pds_osc[1]) == ops[43] + @test length(pds_osc[1]) == 3 + @test repr(pds_osc[3]) == "sp2-br1-rp1-sc1-part3" + + @test length(pds_osc) == 3 + @test isa( + partition_duration(osc, 6), + TimeStruct.PartitionDurationIterator{ + TimeStruct.StratNodeReprOpScenario{ + Int, + TimeStruct.OperationalScenario{Int,SimpleTimes{Int}}, + }, + Int, + FixedProfile{Int}, + }, + ) + @test eltype(partition_duration(osc, 6)) == TimeStruct.StratNodeReprOpScenPart + @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) == 36 + @test length(pds_sp) == 12 + @test length(pds_rp) == 6 + @test all(pds_sp[k] == pds_tl[k+12] for k in 1:3) + @test all(pds_rp[k] == pds_tl[k+12] for k in 1:3) + @test all(pds_osc[k] == pds_tl[k+12] for k in 1:3) + end end end From 405942d6ad53e67c50ac2f0b2f21db5a10a3a0b7 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Tue, 23 Jun 2026 12:13:23 +0200 Subject: [PATCH 2/2] Updated version number --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index b874a28..ae9520a 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "TimeStruct" uuid = "f9ed5ce0-9f41-4eaa-96da-f38ab8df101c" authors = ["Lars Hellemo , Truls.Flatberg "] -version = "0.9.11" +version = "0.9.12" [deps] Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"