DecisionProgramming.jl icon indicating copy to clipboard operation
DecisionProgramming.jl copied to clipboard

Visual representation of influence diagrams and programmatic generation using Mermaid.js

Open jaantollander opened this issue 5 years ago • 2 comments

The visual representation of influence diagrams requires visualizing nodes, information sets, and states. Nodes should have distinct shapes and colors. We should represent them in depth-wise order from left to right. For example:

  • Chance nodes: Circle, light-blue, stroke-width 3
  • Decision nodes: Square, light-green, stroke-width 3
  • Value node: Diamond (rhombus), orange, stroke-width 3
  • Information sets of chance and decision nodes: Normal arrows
  • Information sets of value nodes: Dashed arrows
  • Labels: Node ... <br> States: ...

We can create diagrams programmatically using Mermaid.js:

graph LR
    subgraph Chance and Decision Nodes
        %% Chance nodes
        1((Node: 1 <br> States: 2))
        2((Node: 2 <br> States: 2))
        1 --> 2
        class 1,2 chance

        %% Decision nodes
        3[Node: 3 <br> States: 2]
        4[Node: 4 <br> States: 3]
        1 --> 3
        1 & 2 & 3 --> 4
        class 3,4 decision
    end

    subgraph Value Nodes
        %% Value nodes
        5{Node 5}
        2 & 4 -.-> 5
        class 5 value
    end

    %% Styles
    classDef chance fill:lightblue,stroke:blue,stroke-width:3px;
    classDef decision fill:lightgreen,stroke:green,stroke-width:3px;
    classDef value fill:orange,stroke:darkorange,stroke-width:3px;

Mermaid syntax is explained in the documentation.

Designing influence diagrams is easier using diagrams.net. We can also import Mermaid diagrams to diagrams.net.

jaantollander avatar Sep 20 '20 11:09 jaantollander

We can create mermaid graph using the following code.

using Random
using DecisionProgramming

const Node = Union{ChanceNode, DecisionNode, ValueNode}

function nodes(N::Vector{<:Node}, class::String, edge::String; S::Union{States, Nothing}=nothing)
    lines = []
    for n in N
        if S === nothing
            push!(lines, "$(n.j)[Node: $(n.j)]")
        else
            push!(lines, "$(n.j)[Node: $(n.j) <br> States: $(S[n.j])]")
        end
        if !isempty(n.I_j)
            I_j = join(n.I_j, " & ")
            push!(lines, "$(I_j) $(edge) $(n.j)")
        end
    end
    js = join([n.j for n in N], ",")
    push!(lines, "class $(js) $(class)")
    return join(lines, "\n")
end

function graph(C::Vector{ChanceNode}, D::Vector{DecisionNode}, V::Vector{ValueNode}, S::States)
    return """
    graph LR
    subgraph Chance and Decision Nodes
    %% Chance nodes
    $(nodes(C, "chance", "-->"; S=S))

    %% Decision nodes
    $(nodes(D, "decision", "-->"; S=S))
    end

    subgraph Value Nodes
    %% Value nodes
    $(nodes(V, "value", "-.->"))
    end

    %% Styles
    classDef chance fill:lightblue,stroke:blue,stroke-width:3px;
    classDef decision fill:lightgreen,stroke:green,stroke-width:3px;
    classDef value fill:orange,stroke:darkorange,stroke-width:3px;
    """
end

rng = MersenneTwister(111)

C, D, V = random_diagram(rng, 5, 3, 2, 3, 3)
S = States(rng, [2, 3], length(C) + length(D))
# X = [Probabilities(rng, c, S; n_inactive=0) for c in C]
# Y = [Consequences(rng, v, S, low=-1.0, high=1.5) for v in V]

println(graph(C, D, V, S))

jaantollander avatar Sep 21 '20 16:09 jaantollander

Updated to work with the new version in the interface-update branch and be slightly more visually consistent with the diagrams in our docs.

using Random
using DecisionProgramming


function nodes(diagram::InfluenceDiagram, class::String, edge::String; S::Union{States, Nothing}=nothing)
    lines = []
    if class == "chance"
        N = diagram.C
        left = "(("
        right = "))"
    elseif class == "decision"
        N = diagram.D
        left = "["
        right = "]"
    elseif class == "value"
        N = diagram.V
        left = "{"
        right = "}"
    else
        throw(DomainError("Unknown class $(class)"))
    end
    for n in N
        if S === nothing
            push!(lines, "$(n)$(left)Node: $(diagram.Names[n])$(right)")
        else
            push!(lines, "$(n)$(left)Node: $(diagram.Names[n]) <br> $(S[n]) states$(right)")
        end
        if !isempty(diagram.I_j[n])
            I_j = join(diagram.I_j[n], " & ")
            push!(lines, "$(I_j) $(edge) $(n)")
        end
    end
    js = join([n for n in N], ",")
    push!(lines, "class $(js) $(class)")
    return join(lines, "\n")
end

function graph(diagram::InfluenceDiagram)
    return """
    graph LR
    %% Chance nodes
    $(nodes(diagram, "chance", "-->"; S=diagram.S))

    %% Decision nodes
    $(nodes(diagram, "decision", "-->"; S=diagram.S))

    %% Value nodes
    $(nodes(diagram, "value", "-.->"))

    %% Styles
    classDef chance fill:#F5F5F5 ,stroke:#666666,stroke-width:2px;
    classDef decision fill:#D5E8D4 ,stroke:#82B366,stroke-width:2px;
    classDef value fill:#FFE6CC ,stroke:#D79B00,stroke-width:2px;
    """
end


rng = MersenneTwister(111)

diagram = InfluenceDiagram()
random_diagram!(rng, diagram, 5, 2, 3, 2, 2, [2,3])

println(graph(diagram))

The result looks like this: image

solliolli avatar Oct 07 '21 07:10 solliolli