Skip to content

Provide new constructors for TwoLevelTree #75

Description

@JulStraus

Problem statement

While we have TwoLevelTree included for strategic uncertainty, we only provide a single constructor for the type called regular_tree in which 1) the same time structure is used in all StrategicNodes, 2) all probabilities of the branches are the same, and 3) latter branching is always the same, independent on which branch one is beforehand as shown in the documentation.

This is for a first version fine, limits however the future applicability in cases in which we do not have the same probability.

Proposed approach

It is in general difficult to come up with an idea that is both elegant but simultaneously allows for simple case descriptions. The following approach does not necessarily achieve elegancy, but can represent all potential structures. In relies on the introduction of a new concrete type TreeNode{S, T, OP<:TimeStructure{T}, U} with multiple constructors for ease of use:

using TimeStruct

# Core type with a `duration` within the strategic period, the corresponding time structure
# `ts`, a probability vector `probability` for its children, and a vector of `children.
struct TreeNode{S, T, OP<:TimeStructure{T}, U}
    duration::S
    ts::OP
    probability::Vector{Float64}
    children::Vector{U}
end

# Constructor for the last TreeNode in a branch, i.e., the leaf
function TreeNode(duration::Number, ts::TimeStructure)
    return TreeNode(duration, ts, [1.0], [nothing])
end

# Constructor for a case in which the children time structure does not incorporate uncertainty
function TreeNode(duration::Number, ts::TimeStructure, children::TreeNode)
    return TreeNode(duration, ts, [1.0], children)
end

# Constructor for a case in which all children have the same probability
function TreeNode(duration::Number, ts::TimeStructure, children::Vector{<:TreeNode})
    len_child = length(children)
    return TreeNode(duration, ts, ones(len_child)./len_child, children)
end

# Constructor for a case in which all children are equal (time structure, children, and so on)
# and have the same probability
function TreeNode(duration::Number, ts::TimeStructure, len::Int64, sub_tn::TreeNode)
    return TreeNode(duration, ts, ones(len)./len, fill(sub_tn, len))
end

# Constructor for a case in which all children are equal (time structure, children, and so on),
#  but can have a different probability
function TreeNode(duration::Number, ts::TimeStructure, probability::Vector{<:Float64}, sub_tn::TreeNode)
    len = length(probability)
    return TreeNode(duration, ts, probability, fill(sub_tn, len))
end

This allows us then to construct a TwoLevelTree with different properties:

day = SimpleTimes(24, 1)
week = SimpleTimes(168, 1)
rps = RepresentativePeriods(2, 1, day)
tree_nodes = TreeNode(5, day, [0.2, 0.6, 0.2],
    [
        TreeNode(2, day, [0.2, 0.8],
            [
                TreeNode(2, day),
                TreeNode(2, day, 2, TreeNode(2, day))
            ]
        ),
        TreeNode(2, day, 2, TreeNode(2, week)),
        TreeNode(2, day, TreeNode(2, rps, TreeNode(2, rps))),
    ]
)

while it is not really nice to look at, it provides at least an overview of the individual structure of the Tree. Note that in this case, we can also have a different number of strategic periods in the strategic scenarios.

We can then create the TwoLevelTree through the following functionality

using .TimeStruct: TwoLevelTree, _total_duration, StratNode, _strat_per

# Function for adding the nodes to the tree structure
function add_node(
    nodes::Vector{<:StratNode},
    tn::TreeNode{S, T, OP, U},
    parent::Union{Nothing,StratNode},
    prob::Float64,
    sp::Int64,
    op_per_strat::Real,
) where {S,T,OP<:TimeStructure{T},U}

    oper = tn.ts
    mult_sp = tn.duration * op_per_strat / _total_duration(oper)
    node = StratNode(
        sp,
        count(n -> _strat_per(n) == sp, nodes) + 1,
        tn.duration,
        mult_sp,
        prob,
        parent,
        oper,
    )
    push!(nodes, node)

    for (sub_prob, sub_tn) in zip(tn.probability, tn.children)
        isnothing(sub_tn) && continue

        total_prob = prob * sub_prob
        add_node(nodes, sub_tn, node, total_prob, sp+1, op_per_strat)
    end
    return nodes
end

# Function for creating the instance
function TwoLevelTree(tn::TreeNode; op_per_strat=8760.0)
    nodes = StratNode[]

    nodes = add_node(nodes, tn, nothing, 1.0, 1, op_per_strat)
    nodes = convert(Array{typejoin(typeof.(nodes)...)}, nodes)

    return TwoLevelTree(length(nodes), nodes[1], nodes, op_per_strat)
end

ts = TwoLevelTree(tree_nodes)

It is not the most elegant implementation, but it handles it sufficiently well. One problem I encountered was the type conversion requirement as TwoLevelTree requires the StratNodes to have the same parameters. It was difficult to identify a solution which works as the differing time structures result in typeof(ts.nodes) = Vector{StratNode{Int64, Int64, OP} where OP<:TimeStructure{Int64}}.

Do you have any thoughts on it @hellemo and @trulsf? Shall I create a separate PR and test it a bit further?

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions