From 10c0d7145088a74c5dd77bdd9984fe92da631d01 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Mon, 15 Jun 2026 13:07:48 +0200 Subject: [PATCH 1/4] Fixed a problem with objective_value when using `TwoLevelTree` --- Project.toml | 2 +- src/discount.jl | 9 +++++++-- src/structures.jl | 5 +++-- test/runtests.jl | 32 +++++++++++++++++++++++++++++++- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/Project.toml b/Project.toml index 07ffb0e..b874a28 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.10" +version = "0.9.11" [deps] Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" diff --git a/src/discount.jl b/src/discount.jl index 4719c22..c6a4dc7 100644 --- a/src/discount.jl +++ b/src/discount.jl @@ -135,12 +135,17 @@ end objective_weight(t::Union{TimePeriod,TimeStructurePeriod}, ts::TimeStructure, discount_rate; type = "start", timeunit_to_year = 1.0) objective_weight(t::Union{TimePeriod,TimeStructurePeriod}, disc::Discounter; type = "start") - Calculates the overall objective weight for a time period `t` using a fixed `discount_rate`. The weight considers both discounting, the probability, and potential multiplicity of `t`. The function can be either called using a [`Discounter`](@ref) type or by specifying the parameters (time structure `ts`, `discount_rate` and potentially `timeunit_to_year`) directly. +!!! note "Probabilities" + The objective weight includes the probability, both given through [`OperationalScenarios`](@ref) + or through [`TwoLevelTree`](@ref). This implies that if you use it on, *e.g.*, variables + indexed over strategic periods and calculate these variables through using the [`probability`](@ref) + function on operation period, you have to take care not to have the calculation twice. + There are two types of discounting available: 1. Discounting to the start of the strategic period containing the time period:\n @@ -195,5 +200,5 @@ function _objective_value( type, timeunit_to_year, ) - return discount(t, ts, discount_rate; type, timeunit_to_year) + return probability_branch(t) * discount(t, ts, discount_rate; type, timeunit_to_year) end diff --git a/src/structures.jl b/src/structures.jl index db615e4..449abb2 100644 --- a/src/structures.jl +++ b/src/structures.jl @@ -85,14 +85,15 @@ multiple(t::TimePeriod) = 1.0 """ probability(t::TimePeriod) -Returns the probability associated with the time period. +Returns the probability associated with the time period. This include both the probability +through [`OperationalScenarios`](@ref) and through [`TwoLevelTree`](@ref) structures. """ probability(t::TimePeriod) = 1.0 """ probability_branch(t::Union{TimePeriod, TimeStructurePeriod}) -Returns the branch probability associated with the time period or time structure period +Returns the branch probability associated with the time period or time structure period. """ probability_branch(t::Union{TimePeriod,TimeStructurePeriod}) = 1.0 diff --git a/test/runtests.jl b/test/runtests.jl index d2b2319..78fe158 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1784,7 +1784,9 @@ end using Unitful uniform_years = TwoLevel(10, 1, SimpleTimes(1, 1)) # 10 years with duration of 1 year + uniform_years_tlt = TwoLevelTree(1, ones(Int, 9)*2, SimpleTimes(1, 1)) disc = Discounter(0.04, 1, uniform_years) + disc_tlt = Discounter(0.04, 1, uniform_years_tlt) δ = 1 / 1.04 for (i, t) in enumerate(uniform_years) @@ -1797,46 +1799,74 @@ end ) ≈ 7.8017 atol = 1e-3 @test sum(objective_weight(t, disc) for t in uniform_years) ≈ 8.435 atol = 1e-3 + @test sum(objective_weight(t, disc_tlt) for t in uniform_years_tlt) ≈ 8.435 atol = 1e-3 uniform_day = SimpleTimes(24, 1) periods = TwoLevel(10, 5 * 8760, uniform_day) + periods_tlt = TwoLevelTree(5 * 8760, ones(Int, 9)*2, uniform_day) @test sum( objective_weight(sp, periods, 0.04; timeunit_to_year = 1 / 8760, type = "start") for sp in strat_periods(periods) ) ≈ 4.825 atol = 1e-3 + @test sum( + objective_weight(sp, periods_tlt, 0.04; timeunit_to_year = 1 / 8760, type = "start") for + sp in strat_periods(periods_tlt) + ) ≈ 4.825 atol = 1e-3 @test sum( objective_weight(t, periods, 0.04; timeunit_to_year = 1 / 8760, type = "start") / (length(uniform_day) * probability(t) * multiple(t)) for t in periods ) ≈ 4.825 atol = 1e-3 + @test sum( + objective_weight(t, periods_tlt, 0.04; timeunit_to_year = 1 / 8760, type = "start") / + (length(uniform_day) * probability(t) * multiple(t) / probability_branch(t)) for t in periods_tlt + ) ≈ 4.825 atol = 1e-3 + @test sum( objective_weight(sp, periods, 0.04; timeunit_to_year = 1 / 8760, type = "avg_year") for sp in strat_periods(periods) ) ≈ 4.468 atol = 1e-3 + @test sum( + objective_weight(sp, periods_tlt, 0.04; timeunit_to_year = 1 / 8760, type = "avg_year") + for sp in strat_periods(periods_tlt) + ) ≈ 4.468 atol = 1e-3 @test sum( objective_weight(sp, periods, 0.04; timeunit_to_year = 1 / 8760, type = "avg") for sp in strat_periods(periods) ) ≈ 4.382 atol = 1e-3 + @test sum( + objective_weight(sp, periods_tlt, 0.04; timeunit_to_year = 1 / 8760, type = "avg") for + sp in strat_periods(periods_tlt) + ) ≈ 4.382 atol = 1e-3 oscs = OperationalScenarios(2, uniform_day) periods = TwoLevel(10, 5 * 8760, oscs) + periods_tlt = TwoLevelTree(5 * 8760, ones(Int, 9)*2, oscs) @test sum( objective_weight(t, periods, 0.04; timeunit_to_year = 1 / 8760, type = "start") / (length(oscs) * probability(t) * multiple(t)) for t in periods ) ≈ 4.825 atol = 1e-3 - oscs + @test sum( + objective_weight(t, periods_tlt, 0.04; timeunit_to_year = 1 / 8760, type = "start") / + (length(oscs) * probability(t) * multiple(t) / probability_branch(t)) for t in periods_tlt + ) ≈ 4.825 atol = 1e-3 rps = RepresentativePeriods(2, 1, uniform_day) periods = TwoLevel(10, 5 * 8760, rps) + periods_tlt = TwoLevelTree(5 * 8760, ones(Int, 9)*2, rps) @test sum( objective_weight(t, periods, 0.04; timeunit_to_year = 1 / 8760, type = "start") / (length(rps) * probability(t) * multiple(t)) for t in periods ) ≈ 4.825 atol = 1e-3 + @test sum( + objective_weight(t, periods_tlt, 0.04; timeunit_to_year = 1 / 8760, type = "start") / + (length(rps) * probability(t) * multiple(t) / probability_branch(t)) for t in periods_tlt + ) ≈ 4.825 atol = 1e-3 uniform_day = SimpleTimes(24, 1u"hr") periods_unit = TwoLevel(10, 365.125u"d", uniform_day) From 533a88fea27af27ab7c6f2b0ee60696988f5d51f Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Mon, 15 Jun 2026 13:17:04 +0200 Subject: [PATCH 2/4] Updated the documentation --- docs/src/manual/discount.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/src/manual/discount.md b/docs/src/manual/discount.md index ebfee58..0c60f1d 100644 --- a/docs/src/manual/discount.md +++ b/docs/src/manual/discount.md @@ -27,3 +27,11 @@ df_avg = [discount(sp, ts, 0.05; type = "avg_year") for sp in sps] This approach results in a slightly higher discount factor. To help set up the objective function in a typical optimization problem, there is a utility function [`objective_weight`](@ref) that returns the weight to give a time period in the objective, considering both discount factor, probability, and possible multiplicity. + +!!! note "`objective_weight` for `TwoLevelTree` + You must be careful when you utilize the function [`objective_weight`](@ref) for a [`TimeStruct.StratNode`](@ref). + The function includes the multiplication with the branch probability. + This implies that you must divide the function by [`probability_branch]`(@ref) if you have a variable that is indexed over strategic periods and already includes the branch probability. + + You do not have to do this if your variable is not calculated from variables indexed over operational periods. + This is frequently the case for, *e.g.*, investment variables with one example being [`EnergyModelsInvestments`](https://energymodelsx.github.io/EnergyModelsInvestments.jl/stable/manual/optimization-variables/). From 585e86bdecf5bc7685b45c5e412c7a43bd0f807c Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Mon, 15 Jun 2026 13:20:23 +0200 Subject: [PATCH 3/4] Used JuliaFormatter --- src/profiles.jl | 5 +++-- test/runtests.jl | 53 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/profiles.jl b/src/profiles.jl index f67d575..eeae960 100644 --- a/src/profiles.jl +++ b/src/profiles.jl @@ -426,8 +426,9 @@ end -(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 diff --git a/test/runtests.jl b/test/runtests.jl index 78fe158..7e7c958 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1810,8 +1810,13 @@ end sp in strat_periods(periods) ) ≈ 4.825 atol = 1e-3 @test sum( - objective_weight(sp, periods_tlt, 0.04; timeunit_to_year = 1 / 8760, type = "start") for - sp in strat_periods(periods_tlt) + objective_weight( + sp, + periods_tlt, + 0.04; + timeunit_to_year = 1 / 8760, + type = "start", + ) for sp in strat_periods(periods_tlt) ) ≈ 4.825 atol = 1e-3 @test sum( @@ -1819,18 +1824,28 @@ end (length(uniform_day) * probability(t) * multiple(t)) for t in periods ) ≈ 4.825 atol = 1e-3 @test sum( - objective_weight(t, periods_tlt, 0.04; timeunit_to_year = 1 / 8760, type = "start") / - (length(uniform_day) * probability(t) * multiple(t) / probability_branch(t)) for t in periods_tlt + objective_weight( + t, + periods_tlt, + 0.04; + timeunit_to_year = 1 / 8760, + type = "start", + ) / (length(uniform_day) * probability(t) * multiple(t) / probability_branch(t)) for + t in periods_tlt ) ≈ 4.825 atol = 1e-3 - @test sum( objective_weight(sp, periods, 0.04; timeunit_to_year = 1 / 8760, type = "avg_year") for sp in strat_periods(periods) ) ≈ 4.468 atol = 1e-3 @test sum( - objective_weight(sp, periods_tlt, 0.04; timeunit_to_year = 1 / 8760, type = "avg_year") - for sp in strat_periods(periods_tlt) + objective_weight( + sp, + periods_tlt, + 0.04; + timeunit_to_year = 1 / 8760, + type = "avg_year", + ) for sp in strat_periods(periods_tlt) ) ≈ 4.468 atol = 1e-3 @test sum( @@ -1838,8 +1853,8 @@ end sp in strat_periods(periods) ) ≈ 4.382 atol = 1e-3 @test sum( - objective_weight(sp, periods_tlt, 0.04; timeunit_to_year = 1 / 8760, type = "avg") for - sp in strat_periods(periods_tlt) + objective_weight(sp, periods_tlt, 0.04; timeunit_to_year = 1 / 8760, type = "avg") + for sp in strat_periods(periods_tlt) ) ≈ 4.382 atol = 1e-3 oscs = OperationalScenarios(2, uniform_day) @@ -1851,8 +1866,14 @@ end (length(oscs) * probability(t) * multiple(t)) for t in periods ) ≈ 4.825 atol = 1e-3 @test sum( - objective_weight(t, periods_tlt, 0.04; timeunit_to_year = 1 / 8760, type = "start") / - (length(oscs) * probability(t) * multiple(t) / probability_branch(t)) for t in periods_tlt + objective_weight( + t, + periods_tlt, + 0.04; + timeunit_to_year = 1 / 8760, + type = "start", + ) / (length(oscs) * probability(t) * multiple(t) / probability_branch(t)) for + t in periods_tlt ) ≈ 4.825 atol = 1e-3 rps = RepresentativePeriods(2, 1, uniform_day) @@ -1864,8 +1885,14 @@ end (length(rps) * probability(t) * multiple(t)) for t in periods ) ≈ 4.825 atol = 1e-3 @test sum( - objective_weight(t, periods_tlt, 0.04; timeunit_to_year = 1 / 8760, type = "start") / - (length(rps) * probability(t) * multiple(t) / probability_branch(t)) for t in periods_tlt + objective_weight( + t, + periods_tlt, + 0.04; + timeunit_to_year = 1 / 8760, + type = "start", + ) / (length(rps) * probability(t) * multiple(t) / probability_branch(t)) for + t in periods_tlt ) ≈ 4.825 atol = 1e-3 uniform_day = SimpleTimes(24, 1u"hr") From 3648faaf02b51d3bc8bb4ca4cd090700c4bae30f Mon Sep 17 00:00:00 2001 From: Julian Straus <104911227+JulStraus@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:49:26 +0200 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Truls Flatberg <75753981+trulsf@users.noreply.github.com> --- docs/src/manual/discount.md | 2 +- src/discount.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/manual/discount.md b/docs/src/manual/discount.md index 0c60f1d..fe2d4cb 100644 --- a/docs/src/manual/discount.md +++ b/docs/src/manual/discount.md @@ -31,7 +31,7 @@ To help set up the objective function in a typical optimization problem, there i !!! note "`objective_weight` for `TwoLevelTree` You must be careful when you utilize the function [`objective_weight`](@ref) for a [`TimeStruct.StratNode`](@ref). The function includes the multiplication with the branch probability. - This implies that you must divide the function by [`probability_branch]`(@ref) if you have a variable that is indexed over strategic periods and already includes the branch probability. + This implies that you must divide the function by [`probability_branch`](@ref) if you have a variable that is indexed over strategic periods and already includes the branch probability. You do not have to do this if your variable is not calculated from variables indexed over operational periods. This is frequently the case for, *e.g.*, investment variables with one example being [`EnergyModelsInvestments`](https://energymodelsx.github.io/EnergyModelsInvestments.jl/stable/manual/optimization-variables/). diff --git a/src/discount.jl b/src/discount.jl index c6a4dc7..cf92adf 100644 --- a/src/discount.jl +++ b/src/discount.jl @@ -144,7 +144,7 @@ parameters (time structure `ts`, `discount_rate` and potentially `timeunit_to_ye The objective weight includes the probability, both given through [`OperationalScenarios`](@ref) or through [`TwoLevelTree`](@ref). This implies that if you use it on, *e.g.*, variables indexed over strategic periods and calculate these variables through using the [`probability`](@ref) - function on operation period, you have to take care not to have the calculation twice. + function on operational periods, you have to take care not to have the calculation twice. There are two types of discounting available: