From 06c8e94e8431e32242c9889f319b7d846fc756a3 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 10 Aug 2021 15:22:24 +0300 Subject: [PATCH 001/133] Created Diagram stuct --- src/influence_diagram.jl | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index fc17e339..316aa8a1 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -136,6 +136,7 @@ function States(states::Vector{Tuple{State, Vector{Node}}}) States(S_j) end + """ function validate_influence_diagram(S::States, C::Vector{ChanceNode}, D::Vector{DecisionNode}, V::Vector{ValueNode}) @@ -380,6 +381,25 @@ function (U::DefaultPathUtility)(s::Path) end + +# --- Influence diagram --- + +mutable struct Diagram + Nodes::Array{AbstractNode}[] + S::States + C::Vector{ChanceNode} + D::Vector{DecisionNode} + V::Vector{ValueNode} + X::Vector{Probabilities} + Y::Vector{Consequences} + P::AbstractPathProbability + U::AbstractPathUtility + function Diagram() + new(Nodes = Array{AbstractNode}[]) + end +end + + # --- Local Decision Strategy --- """ From dc7979fb054d13345e81b73ac433b02d3c457698 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 10 Aug 2021 17:24:27 +0300 Subject: [PATCH 002/133] Node adding functions. --- src/DecisionProgramming.jl | 9 ++- src/influence_diagram.jl | 147 ++++++++++++++++++++++++++++++++++++- 2 files changed, 151 insertions(+), 5 deletions(-) diff --git a/src/DecisionProgramming.jl b/src/DecisionProgramming.jl index af1b4e17..2c9753af 100644 --- a/src/DecisionProgramming.jl +++ b/src/DecisionProgramming.jl @@ -23,7 +23,14 @@ export Node, DefaultPathUtility, LocalDecisionStrategy, DecisionStrategy, - validate_influence_diagram + validate_influence_diagram, + Name, + InfluenceDiagram, + BuildDiagram!, + AddNode, + AddChanceNode, + AddDecisionNode, + AddValueNode export DecisionVariables, PathCompatibilityVariables, diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 316aa8a1..115bdfae 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -384,8 +384,19 @@ end # --- Influence diagram --- -mutable struct Diagram - Nodes::Array{AbstractNode}[] +""" + Name = String + +Primitive type for name of node. Alias for `String`. +""" +const Name = String + + +abstract type NodeData end + +mutable struct InfluenceDiagram + Nodes::Vector{NodeData} + Names::Vector{Name} S::States C::Vector{ChanceNode} D::Vector{DecisionNode} @@ -394,11 +405,139 @@ mutable struct Diagram Y::Vector{Consequences} P::AbstractPathProbability U::AbstractPathUtility - function Diagram() - new(Nodes = Array{AbstractNode}[]) + function InfluenceDiagram() + new(Vector{NodeData}()) + end +end + + + + +# --- Node raw data --- + +function validate_node_data(diagram::InfluenceDiagram, name::Name, I_j::Vector{Name}) + if !allunique([map(x -> x.name, diagram.Nodes)..., name]) + throw(DomainError("All node names should be unique.")) + end + + if !allunique(I_j) + throw(DomainError("All information nodes should be unique.")) + end +end + +struct DecisionNodeData <: NodeData + name::Name + I_j::Vector{Name} + states::Vector{Name} + function DecisionNodeData(diagram::InfluenceDiagram, name::Name, I_j::Vector{Name}, states::Vector{Name}) + return new(name, I_j, states) + end +end + +function AddDecisionNode!(diagram::InfluenceDiagram, name::Name, I_j::Vector{Name}, states::Vector{Name}) + validate_node_data(diagram, name, I_j) + push!(diagram.Nodes, DecisionNodeData(name, I_j, states)) +end + +struct ChanceNodeData{N} <: NodeData + name::Name + I_j::Vector{Name} + states::Vector{Name} + probabilities::Array{Float64, N} + function ChanceNodeData(name::Name, I_j::Vector{Name}, states::Vector{Name}, probabilities::Array{Float64, N}) where N + return new{N}(name, I_j, states, probabilities) + end + +end + +function AddChanceNode!(diagram::InfluenceDiagram, name::Name, I_j::Vector{Name}, states::Vector{Name}, probabilities::Array{Float64, N}) where N + validate_node_data(diagram, name, I_j) + push!(diagram.Nodes, ChanceNodeData(name, I_j, states, probabilities)) +end + +struct ValueNodeData{N} <: NodeData + name::Name + I_j::Vector{Name} + consequences::Array{Float64, N} + function ValueNodeData(name::Name, I_j::Vector{Name}, consequences::Array{Float64, N}) where N + return new{N}(name, I_j, consequences) end end +function AddValueNode!(diagram::InfluenceDiagram, name::Name, I_j::Vector{Name}, consequences::Array{Float64, N}) where N + validate_node_data(diagram, name, I_j) + push!(diagram.Nodes, ValueNodeData(name, I_j, Array{Float64}(consequences))) +end + +function deduce_node_indices!(data::Dict{Name, Vector{Name}}, n::Int) + + names = Vector{Name}(undef, n) + indices = Dict{Name, Node}() + nodes = Set{String}() + index = 1 + k = 0 + + while index <= n && k <= n + for (name, I_j) in data + if isempty(setdiff(Set(I_j), nodes)) + names[index] = name + push!(indices, name => index) + push!(nodes, name) + delete!(data, name) + index += 1 + end + end + k += 1 + end + + if index != n + 1 + throw(DomainError("The influence diagram should be acyclic..")) + end + + return names, indices +end + + + +function BuildDiagram!(diagram::InfluenceDiagram; default_probability::Bool=true, default_utility::Bool=true) + + # Number of nodes + nodes = [(n.name for n in diagram.Nodes)...] + n = length(nodes) + + # Check all information sets are subsets of all nodes + information_sets = union((n.I_j for n in diagram.Nodes)...) + if !all(information_sets ⊊ nodes) + throw(DomainError("Each node that is part of an information set should be added as a node.")) + end + + # Build Diagram indices + arc_data = Dict(map(x -> (x.name, x.I_j), diagram.Nodes)) + diagram.Names, indices = deduce_node_indices!(arc_data, n) + + # Declare states + states = Vector{State}() + for j in 1:n + node = diagram.Nodes[findfirst(x -> x.name == diagram.Names[j], diagram.Nodes)] + + if isa(node, ChanceNodeData) + println("yay") + end + + end + + println(states) + + # Declare C, D, V + + + # Declare X, Y + + # Declare P, U + + + +end # --- Local Decision Strategy --- From 9342c462fbb5f81bd5487e227ed38f2eb2c836b4 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 13 Aug 2021 13:14:09 +0300 Subject: [PATCH 003/133] First draft of influence_diagram.jl --- src/influence_diagram.jl | 217 +++++++++++++++++++++++++++++++-------- 1 file changed, 174 insertions(+), 43 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 115bdfae..7f830284 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -144,19 +144,27 @@ Validate influence diagram. """ function validate_influence_diagram(S::States, C::Vector{ChanceNode}, D::Vector{DecisionNode}, V::Vector{ValueNode}) n = length(C) + length(D) + # in validate_node_data if length(S) != n throw(DomainError("Each change and decision node should have states.")) end + + # in deduce_node_indices logic... if Set(c.j for c in C) ∪ Set(d.j for d in D) != Set(1:n) throw(DomainError("Union of change and decision nodes should be {1,...,n}.")) end + + # in deduce_node_indices logic... if Set(v.j for v in V) != Set((n+1):(n+length(V))) throw(DomainError("Values nodes should be {n+1,...,n+|V|}.")) end + + # in deduce_node_indices I_V = union((v.I_j for v in V)...) if !(I_V ⊆ Set(1:n)) throw(DomainError("Each information set I(v) for value node v should be a subset of C∪D.")) end + # in deduce_node_indices # Check for redundant nodes. leaf_nodes = setdiff(1:n, (c.I_j for c in C)..., (d.I_j for d in D)...) for i in leaf_nodes @@ -164,6 +172,7 @@ function validate_influence_diagram(S::States, C::Vector{ChanceNode}, D::Vector{ @warn("Chance or decision node $i is redundant.") end end + # in validate_node_data for v in V if isempty(v.I_j) @warn("Value node $(v.j) is redundant.") @@ -397,6 +406,7 @@ abstract type NodeData end mutable struct InfluenceDiagram Nodes::Vector{NodeData} Names::Vector{Name} + I_j::Vector{Vector{State}} S::States C::Vector{ChanceNode} D::Vector{DecisionNode} @@ -415,13 +425,33 @@ end # --- Node raw data --- -function validate_node_data(diagram::InfluenceDiagram, name::Name, I_j::Vector{Name}) +function validate_node_data(diagram::InfluenceDiagram, + name::Name, + I_j::Vector{Name}; + value_node::Bool=false, + states::Vector{Name}=Vector{Name}()) if !allunique([map(x -> x.name, diagram.Nodes)..., name]) throw(DomainError("All node names should be unique.")) end if !allunique(I_j) - throw(DomainError("All information nodes should be unique.")) + throw(DomainError("All nodes in an information set should be unique.")) + end + + if !allunique([name, I_j...]) + throw(DomainError("Node should not be included in its own information set.")) + end + + if !value_node + if length(states) < 2 + throw(DomainError("Each chance and decision node should have more than one state.")) + end + end + + if value_node + if isempty(I_j) + @warn("Value node $name is redundant.") + end end end @@ -429,13 +459,16 @@ struct DecisionNodeData <: NodeData name::Name I_j::Vector{Name} states::Vector{Name} - function DecisionNodeData(diagram::InfluenceDiagram, name::Name, I_j::Vector{Name}, states::Vector{Name}) + function DecisionNodeData(name::Name, I_j::Vector{Name}, states::Vector{Name}) return new(name, I_j, states) end end -function AddDecisionNode!(diagram::InfluenceDiagram, name::Name, I_j::Vector{Name}, states::Vector{Name}) - validate_node_data(diagram, name, I_j) +function AddDecisionNode!(diagram::InfluenceDiagram, + name::Name, + I_j::Vector{Name}, + states::Vector{Name}) + validate_node_data(diagram, name, I_j, states = states) push!(diagram.Nodes, DecisionNodeData(name, I_j, states)) end @@ -450,8 +483,11 @@ struct ChanceNodeData{N} <: NodeData end -function AddChanceNode!(diagram::InfluenceDiagram, name::Name, I_j::Vector{Name}, states::Vector{Name}, probabilities::Array{Float64, N}) where N - validate_node_data(diagram, name, I_j) +function AddChanceNode!(diagram::InfluenceDiagram, + name::Name, I_j::Vector{Name}, + states::Vector{Name}, + probabilities::Array{Float64, N}) where N + validate_node_data(diagram, name, I_j, states = states) push!(diagram.Nodes, ChanceNodeData(name, I_j, states, probabilities)) end @@ -464,42 +500,106 @@ struct ValueNodeData{N} <: NodeData end end -function AddValueNode!(diagram::InfluenceDiagram, name::Name, I_j::Vector{Name}, consequences::Array{Float64, N}) where N - validate_node_data(diagram, name, I_j) +function AddValueNode!(diagram::InfluenceDiagram, + name::Name, + I_j::Vector{Name}, + consequences::Array{Float64, N}) where N + validate_node_data(diagram, name, I_j, value_node = true) push!(diagram.Nodes, ValueNodeData(name, I_j, Array{Float64}(consequences))) end -function deduce_node_indices!(data::Dict{Name, Vector{Name}}, n::Int) - names = Vector{Name}(undef, n) +function deduce_node_indices(Nodes::Vector{NodeData}) + + # Chance and decision nodes + C_and_D = filter(x -> !isa(x, ValueNodeData), Nodes) + n_CD = length(C_and_D) + + # Value nodes + V = filter(x -> isa(x, ValueNodeData), Nodes) + n_V = length(V) + + + # Validating node structure + if !isempty(Set(j.I_j for j in C_and_D) ∩ Set(v.name for v in V)) + throw(DomainError("Information set I(j) for chance or decision node j should not include value nodes.")) + end + + if !isempty(Set(v.I_j for v in V) ∩ Set(v.name for v in V)) + throw(DomainError("Information set I(v) for value nodes v should not include value nodes.")) + end + # Check for redundant chance or decision nodes. + last_CD_nodes = setdiff((j.name for j in C_and_D), (j.I_j for j in C_and_D)...) + for i in last_CD_nodes + if i ∉ union((v.I_j for v in V)...) + @warn("Chance or decision node $i is redundant.") + end + end + + + # Declare vectors for results (final resting place InfluenceDiagram.Names and InfluenceDiagram.I_j) + Names = Vector{Name}(undef, n_CD+n_V) + I_js = Vector{Vector{Node}}(undef, n_CD+n_V) + + # Declare helper collections indices = Dict{Name, Node}() - nodes = Set{String}() + indexed_nodes = Set{Name}() + + # Declare index for indexing nodes and loop iteration counter k index = 1 - k = 0 - - while index <= n && k <= n - for (name, I_j) in data - if isempty(setdiff(Set(I_j), nodes)) - names[index] = name - push!(indices, name => index) - push!(nodes, name) - delete!(data, name) + k = 1 + + while index <= n_CD+n_V && k == index + + # First index nodes C and D nodes + if index <= n_CD + for j in C_and_D + # Give bindex if node does not already have an index and all of its I_j have an index + if j.name ∉ indexed_nodes && Set(j.I_j) ⊆ indexed_nodes + # Update helper collections + push!(indices, j.name => index) + push!(indexed_nodes, j.name) + + # Update results + Names[index] = j.name + I_js[index] = map(x -> indices[x], j.I_j) + + # Increase index + index += 1 + end + end + + # After indexing all C and D nodes, index value nodes + else + + for v in V + # Update results + Names[index] = v.name + I_js[index] = map(x -> indices[x], v.I_j) + + # Increase index index += 1 end end - k += 1 + + # If new nodes have not been indexed during this iteration, terminate while loop + if index <= k + k += 1 + else + k = index + end end - if index != n + 1 - throw(DomainError("The influence diagram should be acyclic..")) + + if index != k + throw(DomainError("The influence diagram should be acyclic.")) end - return names, indices + return Names, I_js end - -function BuildDiagram!(diagram::InfluenceDiagram; default_probability::Bool=true, default_utility::Bool=true) +function GenerateDiagram!(diagram::InfluenceDiagram; default_probability::Bool=true, default_utility::Bool=true) # Number of nodes nodes = [(n.name for n in diagram.Nodes)...] @@ -511,31 +611,62 @@ function BuildDiagram!(diagram::InfluenceDiagram; default_probability::Bool=true throw(DomainError("Each node that is part of an information set should be added as a node.")) end - # Build Diagram indices - arc_data = Dict(map(x -> (x.name, x.I_j), diagram.Nodes)) - diagram.Names, indices = deduce_node_indices!(arc_data, n) + # Deduce indices for nodes + diagram.Names, diagram.I_j = deduce_node_indices(diagram.Nodes) - # Declare states + # Declare states, C, D, V, X, Y states = Vector{State}() - for j in 1:n - node = diagram.Nodes[findfirst(x -> x.name == diagram.Names[j], diagram.Nodes)] + diagram.C = Vector{ChanceNode}() + diagram.D = Vector{DecisionNode}() + diagram.V = Vector{ValueNode}() + diagram.X = Vector{Probabilities}() + diagram.Y = Vector{Consequences}() - if isa(node, ChanceNodeData) - println("yay") - end - - end - - println(states) + for (j, name) in enumerate(diagram.Names) + node = diagram.Nodes[findfirst(x -> x.name == diagram.Names[j], diagram.Nodes)] - # Declare C, D, V + if isa(node, DecisionNodeData) + push!(states, length(node.states)) + push!(diagram.D, DecisionNode(j, diagram.I_j[j])) + elseif isa(node, ChanceNodeData) + push!(states, length(node.states)) + push!(diagram.C, ChanceNode(j, diagram.I_j[j])) - # Declare X, Y + # Check dimensions of probabiltiies match states of (I_j, j) + if size(node.probabilities) == Tuple((states[n] for n in (diagram.I_j[j]..., j))) + push!(diagram.X, Probabilities(j, node.probabilities)) + else + # TODO rephrase this error message + throw(DomainError("The dimensions of the probability matrix of node $name should match the number of states its information set and it has. In this case ", Tuple((states[n] for n in (diagram.I_j[j]..., j))), ".")) + end + elseif isa(node, ValueNodeData) + push!(diagram.V, ValueNode(j, diagram.I_j[j])) + + # Check dimensions of consequences match states of I_j + if size(node.consequences) == Tuple((states[n] for n in diagram.I_j[j])) + push!(diagram.Y, Consequences(j, node.consequences)) + else + # TODO rephrase this error message + throw(DomainError("The dimensions of the consequences matrix of node $name should match the number of states its information set has. In this case ", Tuple((states[n] for n in (diagram.I_j[j]..., j))), ".")) + end - # Declare P, U + end + end + # TODO ask Olli about this, if the States should be changed + diagram.S = States(states) + # Validate influence diagram + validate_influence_diagram(diagram.S, diagram.C, diagram.D, diagram.V) + sort!.((diagram.C, diagram.D, diagram.V, diagram.X, diagram.Y), by = x -> x.j) + # Declare P and U if defaults are used + if default_probability + diagram.P = DefaultPathProbability(diagram.C, diagram.X) + end + if default_utility + diagram.U = DefaultPathUtility(diagram.V, diagram.Y) + end end From 6d194c5cf620f9beacc8eb42095b1eca54f1b972 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 13 Aug 2021 13:15:06 +0300 Subject: [PATCH 004/133] Used car buyer example with new interface. --- examples/used_car_buyer.jl | 56 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/examples/used_car_buyer.jl b/examples/used_car_buyer.jl index 5ce895ef..fddbeeac 100644 --- a/examples/used_car_buyer.jl +++ b/examples/used_car_buyer.jl @@ -1,7 +1,61 @@ + using Logging using JuMP, Gurobi using DecisionProgramming +@info("Creating the influence diagram.") +diagram = InfluenceDiagram() + +AddChanceNode!(diagram, "O", Vector{Name}(), ["lemon", "peach"], [0.2, 0.8]) + +X_R = zeros(2, 2, 3) +X_R[1, 1, :] = [1,0,0] +X_R[1, 2, :] = [0,1,0] +X_R[2, 1, :] = [1,0,0] +X_R[2, 2, :] = [0,0,1] +AddChanceNode!(diagram, "R", ["O", "T"], ["no test", "lemon", "peach"], X_R) +AddDecisionNode!(diagram, "T", Vector{Name}(), ["no test", "test"]) +AddDecisionNode!(diagram, "A", ["R"], ["buy without guarantee", "buy with guarantee", "don't buy"]) +AddValueNode!(diagram, "V1", ["T"], [0.0, -25.0]) +AddValueNode!(diagram, "V2", ["A"], [100.0, 40.0, 0.0]) +AddValueNode!(diagram, "V3", ["O", "A"], [-200.0 0.0 0.0; -40.0 -20.0 0.0]) + +GenerateDiagram!(diagram) + + +@info("Creating the decision model.") +model = Model() +z = DecisionVariables(model, diagram) +x_s = PathCompatibilityVariables(model, diagram, z) +EV = expected_value(model, diagram, x_s) +@objective(model, Max, EV) + +@info("Starting the optimization process.") +optimizer = optimizer_with_attributes( + () -> Gurobi.Optimizer(Gurobi.Env()), + "IntFeasTol" => 1e-9, +) +set_optimizer(model, optimizer) +optimize!(model) + +@info("Extracting results.") +Z = DecisionStrategy(z) + +@info("Printing decision strategy:") +print_decision_strategy(S, Z) + +@info("Computing utility distribution.") +udist = UtilityDistribution(S, P, U, Z) + +@info("Printing utility distribution.") +print_utility_distribution(udist) + +@info("Printing expected utility.") +print_statistics(udist) + + +#= + const O = 1 # Chance node: lemon or peach const T = 2 # Decision node: pay stranger for advice const R = 3 # Chance node: observation of state of the car @@ -24,6 +78,7 @@ V = Vector{ValueNode}() X = Vector{Probabilities}() Y = Vector{Consequences}() + I_O = Vector{Node}() X_O = [0.2, 0.8] push!(C, ChanceNode(O, I_O)) @@ -95,3 +150,4 @@ print_utility_distribution(udist) @info("Printing expected utility.") print_statistics(udist) +=# From 6201db1629d6290dccba7f979aed75ee1d97fbb8 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 13 Aug 2021 13:15:48 +0300 Subject: [PATCH 005/133] Added new functions and types to DecisionProgramming.jl --- src/DecisionProgramming.jl | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/DecisionProgramming.jl b/src/DecisionProgramming.jl index 2c9753af..578cf4f1 100644 --- a/src/DecisionProgramming.jl +++ b/src/DecisionProgramming.jl @@ -26,11 +26,14 @@ export Node, validate_influence_diagram, Name, InfluenceDiagram, - BuildDiagram!, - AddNode, - AddChanceNode, - AddDecisionNode, - AddValueNode + GenerateDiagram!, + NodeData, + DecisionNodeData, + ChanceNodeData, + ValueNodeData, + AddDecisionNode!, + AddChanceNode!, + AddValueNode! export DecisionVariables, PathCompatibilityVariables, From 01ea4028ecde366b2cc25eea9aef611f4a5cde70 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 13 Aug 2021 13:28:53 +0300 Subject: [PATCH 006/133] Updated printing call. --- examples/used_car_buyer.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/used_car_buyer.jl b/examples/used_car_buyer.jl index fddbeeac..bf9c949b 100644 --- a/examples/used_car_buyer.jl +++ b/examples/used_car_buyer.jl @@ -42,10 +42,10 @@ optimize!(model) Z = DecisionStrategy(z) @info("Printing decision strategy:") -print_decision_strategy(S, Z) +print_decision_strategy(diagram, Z) @info("Computing utility distribution.") -udist = UtilityDistribution(S, P, U, Z) +udist = UtilityDistribution(diagram.S, diagram.P, diagram.U, Z) @info("Printing utility distribution.") print_utility_distribution(udist) From 598be0a33f03ac00a316b3f8d1e23d0a92cd0c2a Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 13 Aug 2021 13:33:28 +0300 Subject: [PATCH 007/133] First round of edits on decision_model.jl --- src/decision_model.jl | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/decision_model.jl b/src/decision_model.jl index 52d64def..d32d9cf1 100644 --- a/src/decision_model.jl +++ b/src/decision_model.jl @@ -37,8 +37,8 @@ Create decision variables and constraints. z = DecisionVariables(model, S, D) ``` """ -function DecisionVariables(model::Model, S::States, D::Vector{DecisionNode}; names::Bool=false, name::String="z") - DecisionVariables(D, [decision_variable(model, S, d, (names ? "$(name)_$(d.j)$(s)" : "")) for d in D]) +function DecisionVariables(model::Model, diagram::InfluenceDiagram; names::Bool=false, name::String="z") + DecisionVariables(diagram.D, [decision_variable(model, diagram.S, d, (names ? "$(name)_$(d.j)$(s)" : "")) for d in diagram.D]) end function is_forbidden(s::Path, forbidden_paths::Vector{ForbiddenPath}) @@ -123,9 +123,8 @@ x_s = PathCompatibilityVariables(model, z, S, P; probability_cut = false) ``` """ function PathCompatibilityVariables(model::Model, - z::DecisionVariables, - S::States, - P::AbstractPathProbability; + diagram::InfluenceDiagram, + z::DecisionVariables; names::Bool=false, name::String="x", forbidden_paths::Vector{ForbiddenPath}=ForbiddenPath[], @@ -137,22 +136,22 @@ function PathCompatibilityVariables(model::Model, end # Create path compatibility variable for each effective path. - N = length(S) + N = length(diagram.S) variables_x_s = Dict{Path{N}, VariableRef}( s => path_compatibility_variable(model, z, (names ? "$(name)$(s)" : "")) - for s in paths(S, fixed) - if !iszero(P(s)) && !is_forbidden(s, forbidden_paths) + for s in paths(diagram.S, fixed) + if !iszero(diagram.P(s)) && !is_forbidden(s, forbidden_paths) ) x_s = PathCompatibilityVariables{N}(variables_x_s) # Add decision strategy constraints for each decision node for (d, z_d) in zip(z.D, z.z) - decision_strategy_constraint(model, S, d, z.D, z_d, x_s) + decision_strategy_constraint(model, diagram.S, d, z.D, z_d, x_s) end if probability_cut - @constraint(model, sum(x * P(s) for (s, x) in x_s) == 1.0) + @constraint(model, sum(x * diagram.P(s) for (s, x) in x_s) == 1.0) end x_s @@ -172,16 +171,16 @@ lazy_probability_cut(model, x_s, P) Remember to set lazy constraints on in the solver parameters, unless your solver does this automatically. Note that Gurobi does this automatically. """ -function lazy_probability_cut(model::Model, x_s::PathCompatibilityVariables, P::AbstractPathProbability) +function lazy_probability_cut(model::Model, diagram::InfluenceDiagram, x_s::PathCompatibilityVariables) # August 2021: The current implementation of JuMP doesn't allow multiple callback functions of the same type (e.g. lazy) # (see https://github.com/jump-dev/JuMP.jl/issues/2642) # What this means is that if you come up with a new lazy cut, you must replace this # function with a more general function (see discussion and solution in https://github.com/gamma-opt/DecisionProgramming.jl/issues/20) function probability_cut(cb_data) - xsum = sum(callback_value(cb_data, x) * P(s) for (s, x) in x_s) + xsum = sum(callback_value(cb_data, x) * diagram.P(s) for (s, x) in x_s) if !isapprox(xsum, 1.0) - con = @build_constraint(sum(x * P(s) for (s, x) in x_s) == 1.0) + con = @build_constraint(sum(x * diagram.P(s) for (s, x) in x_s) == 1.0) MOI.submit(model, MOI.LazyConstraint(cb_data), con) end end @@ -205,6 +204,8 @@ true struct PositivePathUtility <: AbstractPathUtility U::AbstractPathUtility min::Float64 + # TODO decide if P and U should be in struct InfluenceDiagram, if so then change this + # to outer construction function and make it update diagram.U function PositivePathUtility(S::States, U::AbstractPathUtility) u_min = minimum(U(s) for s in paths(S)) new(U, u_min) @@ -228,6 +229,8 @@ true struct NegativePathUtility <: AbstractPathUtility U::AbstractPathUtility max::Float64 + # TODO decide if P and U should be in struct InfluenceDiagram, if so then change this + # to outer construction function and make it update diagram.U function NegativePathUtility(S::States, U::AbstractPathUtility) u_max = maximum(U(s) for s in paths(S)) new(U, u_max) @@ -261,16 +264,15 @@ EV = expected_value(model, x_s, U, P; probability_scale_factor = 10.0) ``` """ function expected_value(model::Model, - x_s::PathCompatibilityVariables, - U::AbstractPathUtility, - P::AbstractPathProbability; + diagram::InfluenceDiagram, + x_s::PathCompatibilityVariables; probability_scale_factor::Float64=1.0) if probability_scale_factor ≤ 0 throw(DomainError("The probability_scale_factor must be greater than 0.")) end - @expression(model, sum(P(s) * x * U(s) * probability_scale_factor for (s, x) in x_s)) + @expression(model, sum(diagram.P(s) * x * diagram.U(s) * probability_scale_factor for (s, x) in x_s)) end """ @@ -302,6 +304,7 @@ CVaR = conditional_value_at_risk(model, x_s, U, P, α) CVaR = conditional_value_at_risk(model, x_s, U, P, α; probability_scale_factor = 10.0) ``` """ +# TODO decide if P and U should be in struct InfluenceDiagram, if so then update this function function conditional_value_at_risk(model::Model, x_s::PathCompatibilityVariables{N}, U::AbstractPathUtility, From 8484a9a3208e859e49bb8af04889751bb9b52ed0 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 13 Aug 2021 13:33:56 +0300 Subject: [PATCH 008/133] Edited the decision strategy printing function to use InfluenceDiagram --- src/printing.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/printing.jl b/src/printing.jl index 611ef2ff..2c20ac1c 100644 --- a/src/printing.jl +++ b/src/printing.jl @@ -11,9 +11,9 @@ Print decision strategy. print_decision_strategy(S, Z) ``` """ -function print_decision_strategy(S::States, Z::DecisionStrategy) +function print_decision_strategy(diagram::InfluenceDiagram, Z::DecisionStrategy) for (d, Z_j) in zip(Z.D, Z.Z_j) - a1 = vec(collect(paths(S[d.I_j]))) + a1 = vec(collect(paths(diagram.S[d.I_j]))) a2 = [Z_j(s_I) for s_I in a1] labels = fill("States", length(a1)) df = DataFrame(labels = labels, a1 = a1, a2 = a2) From 3e44b1a9d7c5196912485d9b1922ce01ffcac7f7 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 13 Aug 2021 14:25:58 +0300 Subject: [PATCH 009/133] Updated pig_breeding example. --- examples/pig_breeding.jl | 133 +++++++++++++-------------------------- 1 file changed, 45 insertions(+), 88 deletions(-) diff --git a/examples/pig_breeding.jl b/examples/pig_breeding.jl index bf26415c..ab5b8924 100644 --- a/examples/pig_breeding.jl +++ b/examples/pig_breeding.jl @@ -3,97 +3,53 @@ using JuMP, Gurobi using DecisionProgramming const N = 4 -const health = [3*k - 2 for k in 1:N] -const test = [3*k - 1 for k in 1:(N-1)] -const treat = [3*k for k in 1:(N-1)] -const cost = [(3*N - 2) + k for k in 1:(N-1)] -const price = [(3*N - 2) + N] -const health_states = ["ill", "healthy"] -const test_states = ["positive", "negative"] -const treat_states = ["treat", "pass"] -@info("Creating the influence diagram.") -S = States([ - (length(health_states), health), - (length(test_states), test), - (length(treat_states), treat), -]) -C = Vector{ChanceNode}() -D = Vector{DecisionNode}() -V = Vector{ValueNode}() -X = Vector{Probabilities}() -Y = Vector{Consequences}() - -for j in health[[1]] - I_j = Vector{Node}() - X_j = zeros(S[I_j]..., S[j]) - X_j[1] = 0.1 - X_j[2] = 1.0 - X_j[1] - push!(C, ChanceNode(j, I_j)) - push!(X, Probabilities(j, X_j)) -end - -for (i, k, j) in zip(health[1:end-1], treat, health[2:end]) - I_j = [i, k] - X_j = zeros(S[I_j]..., S[j]) - X_j[2, 2, 1] = 0.2 - X_j[2, 2, 2] = 1.0 - X_j[2, 2, 1] - X_j[2, 1, 1] = 0.1 - X_j[2, 1, 2] = 1.0 - X_j[2, 1, 1] - X_j[1, 2, 1] = 0.9 - X_j[1, 2, 2] = 1.0 - X_j[1, 2, 1] - X_j[1, 1, 1] = 0.5 - X_j[1, 1, 2] = 1.0 - X_j[1, 1, 1] - push!(C, ChanceNode(j, I_j)) - push!(X, Probabilities(j, X_j)) -end - -for (i, j) in zip(health, test) - I_j = [i] - X_j = zeros(S[I_j]..., S[j]) - X_j[1, 1] = 0.8 - X_j[1, 2] = 1.0 - X_j[1, 1] - X_j[2, 2] = 0.9 - X_j[2, 1] = 1.0 - X_j[2, 2] - push!(C, ChanceNode(j, I_j)) - push!(X, Probabilities(j, X_j)) -end -for (i, j) in zip(test, treat) - I_j = [i] - push!(D, DecisionNode(j, I_j)) -end - -for (i, j) in zip(treat, cost) - I_j = [i] - Y_j = zeros(S[I_j]...) - Y_j[1] = -100 - Y_j[2] = 0 - push!(V, ValueNode(j, I_j)) - push!(Y, Consequences(j, Y_j)) -end - -for (i, j) in zip(health[end], price) - I_j = [i] - Y_j = zeros(S[I_j]...) - Y_j[1] = 300 - Y_j[2] = 1000 - push!(V, ValueNode(j, I_j)) - push!(Y, Consequences(j, Y_j)) +@info("Creating the influence diagram.") +diagram = InfluenceDiagram() + +AddChanceNode!(diagram, "H1", Vector{Name}(), ["ill", "healthy"], [0.1, 0.9]) + +# Declare proability matrix for health nodes +X_H = zeros(2, 2, 2) +X_H[2, 2, 1] = 0.2 +X_H[2, 2, 2] = 1.0 - X_H[2, 2, 1] +X_H[2, 1, 1] = 0.1 +X_H[2, 1, 2] = 1.0 - X_H[2, 1, 1] +X_H[1, 2, 1] = 0.9 +X_H[1, 2, 2] = 1.0 - X_H[1, 2, 1] +X_H[1, 1, 1] = 0.5 +X_H[1, 1, 2] = 1.0 - X_H[1, 1, 1] + +# Declare proability matrix for test results nodes +X_T = zeros(2, 2) +X_T[1, 1] = 0.8 +X_T[1, 2] = 1.0 - X_T[1, 1] +X_T[2, 2] = 0.9 +X_T[2, 1] = 1.0 - X_T[2, 2] + +for i in 1:N-1 + # Testing result + AddChanceNode!(diagram, "T$i", ["H$i"], ["positive", "negative"], X_T) + # Decision to treat + AddDecisionNode!(diagram, "D$i", ["T$i"], ["treat", "pass"]) + # Cost of treatment + AddValueNode!(diagram, "C$i", ["D$i"], [-100.0, 0.0]) + # Health of next period + AddChanceNode!(diagram, "H$(i+1)", ["H$(i)", "D$(i)"], ["ill", "healthy"], X_H) end -validate_influence_diagram(S, C, D, V) -sort!.((C, D, V, X, Y), by = x -> x.j) +# Selling price +AddValueNode!(diagram, "SP", ["H4"], [300.0, 1000.0]) -P = DefaultPathProbability(C, X) -U = DefaultPathUtility(V, Y) +GenerateDiagram!(diagram) @info("Creating the decision model.") -U⁺ = PositivePathUtility(S, U) +#U⁺ = PositivePathUtility(S, U) model = Model() -z = DecisionVariables(model, S, D) -x_s = PathCompatibilityVariables(model, z, S, P, probability_cut = false) -EV = expected_value(model, x_s, U⁺, P) +z = DecisionVariables(model, diagram) +x_s = PathCompatibilityVariables(model, diagram, z, probability_cut = true) +EV = expected_value(model, diagram, x_s) @objective(model, Max, EV) @info("Starting the optimization process.") @@ -108,10 +64,11 @@ optimize!(model) Z = DecisionStrategy(z) @info("Printing decision strategy:") -print_decision_strategy(S, Z) +print_decision_strategy(diagram, Z) +#= @info("State probabilities:") -sprobs = StateProbabilities(S, P, Z) +sprobs = StateProbabilities(diagram.S, diagram.P, Z) print_state_probabilities(sprobs, health) print_state_probabilities(sprobs, test) print_state_probabilities(sprobs, treat) @@ -119,14 +76,14 @@ print_state_probabilities(sprobs, treat) @info("Conditional state probabilities") node = 1 for state in 1:2 - sprobs2 = StateProbabilities(S, P, Z, node, state, sprobs) + sprobs2 = StateProbabilities(diagram.S, diagram.P, Z, node, state, sprobs) print_state_probabilities(sprobs2, health) print_state_probabilities(sprobs2, test) print_state_probabilities(sprobs2, treat) end - +=# @info("Computing utility distribution.") -udist = UtilityDistribution(S, P, U, Z) +udist = UtilityDistribution(diagram.S, diagram.P, diagram.U, Z) @info("Printing utility distribution.") print_utility_distribution(udist) From 199b0d8e9c0d96f844844e9c364e353e76e3d13e Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 13 Aug 2021 14:35:50 +0300 Subject: [PATCH 010/133] Fixed hard coded N=4 --- examples/pig_breeding.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pig_breeding.jl b/examples/pig_breeding.jl index ab5b8924..e7e289da 100644 --- a/examples/pig_breeding.jl +++ b/examples/pig_breeding.jl @@ -40,7 +40,7 @@ for i in 1:N-1 end # Selling price -AddValueNode!(diagram, "SP", ["H4"], [300.0, 1000.0]) +AddValueNode!(diagram, "SP", ["H$N"], [300.0, 1000.0]) GenerateDiagram!(diagram) From 8e8e2ab937d04b556d21d465bf1ec3475ed0a1d4 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 13 Aug 2021 14:35:50 +0300 Subject: [PATCH 011/133] Fixed hard coded N=4 --- examples/pig_breeding.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/pig_breeding.jl b/examples/pig_breeding.jl index ab5b8924..6dce7dce 100644 --- a/examples/pig_breeding.jl +++ b/examples/pig_breeding.jl @@ -4,7 +4,6 @@ using DecisionProgramming const N = 4 - @info("Creating the influence diagram.") diagram = InfluenceDiagram() @@ -40,7 +39,7 @@ for i in 1:N-1 end # Selling price -AddValueNode!(diagram, "SP", ["H4"], [300.0, 1000.0]) +AddValueNode!(diagram, "SP", ["H$N"], [300.0, 1000.0]) GenerateDiagram!(diagram) From 30f9796c64e3e718a880a453e469b73fb1da7446 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 17 Aug 2021 11:10:58 +0300 Subject: [PATCH 012/133] Improved influence diagram validation criteria. --- src/influence_diagram.jl | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 7f830284..a83492c9 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -521,18 +521,25 @@ function deduce_node_indices(Nodes::Vector{NodeData}) # Validating node structure - if !isempty(Set(j.I_j for j in C_and_D) ∩ Set(v.name for v in V)) - throw(DomainError("Information set I(j) for chance or decision node j should not include value nodes.")) + if n_CD == 0 + throw(DomainError("The influence diagram must have chance or decision nodes.")) end - - if !isempty(Set(v.I_j for v in V) ∩ Set(v.name for v in V)) - throw(DomainError("Information set I(v) for value nodes v should not include value nodes.")) + if !(union((n.I_j for n in Nodes)...) ⊊ Set(n.name for n in Nodes)) + throw(DomainError("Each node that is part of an information set should be added as a node.")) + end + # Checking the information sets of C and D nodes + if !isempty(union((j.I_j for j in C_and_D)...) ∩ Set(v.name for v in V)) + throw(DomainError("Information sets should not include any value nodes.")) + end + # Checking the information sets of V nodes + if !isempty(V) && !isempty(union((v.I_j for v in V)...) ∩ Set(v.name for v in V)) + throw(DomainError("Information sets should not include any value nodes.")) end # Check for redundant chance or decision nodes. last_CD_nodes = setdiff((j.name for j in C_and_D), (j.I_j for j in C_and_D)...) for i in last_CD_nodes - if i ∉ union((v.I_j for v in V)...) - @warn("Chance or decision node $i is redundant.") + if !isempty(V) && i ∉ union((v.I_j for v in V)...) + @warn("Node $i is redundant.") end end @@ -605,12 +612,6 @@ function GenerateDiagram!(diagram::InfluenceDiagram; default_probability::Bool=t nodes = [(n.name for n in diagram.Nodes)...] n = length(nodes) - # Check all information sets are subsets of all nodes - information_sets = union((n.I_j for n in diagram.Nodes)...) - if !all(information_sets ⊊ nodes) - throw(DomainError("Each node that is part of an information set should be added as a node.")) - end - # Deduce indices for nodes diagram.Names, diagram.I_j = deduce_node_indices(diagram.Nodes) From 0363b501e389a4ccfab736cc741d3699e0b9a36d Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 17 Aug 2021 16:04:31 +0300 Subject: [PATCH 013/133] Fixed error messages that catch erroneous probability and consequence matrices. --- src/influence_diagram.jl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index a83492c9..51c3bec4 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -623,6 +623,7 @@ function GenerateDiagram!(diagram::InfluenceDiagram; default_probability::Bool=t diagram.X = Vector{Probabilities}() diagram.Y = Vector{Consequences}() + # Fill states, C, D, V, X, Y for (j, name) in enumerate(diagram.Names) node = diagram.Nodes[findfirst(x -> x.name == diagram.Names[j], diagram.Nodes)] @@ -638,8 +639,7 @@ function GenerateDiagram!(diagram::InfluenceDiagram; default_probability::Bool=t if size(node.probabilities) == Tuple((states[n] for n in (diagram.I_j[j]..., j))) push!(diagram.X, Probabilities(j, node.probabilities)) else - # TODO rephrase this error message - throw(DomainError("The dimensions of the probability matrix of node $name should match the number of states its information set and it has. In this case ", Tuple((states[n] for n in (diagram.I_j[j]..., j))), ".")) + throw(DomainError("The dimensions of a probability matrix should match the node's states' and information states' cardinality. Expected $(Tuple((states[n] for n in (diagram.I_j[j]..., j)))) for node $name, got $(size(node.probabilities)).")) end elseif isa(node, ValueNodeData) push!(diagram.V, ValueNode(j, diagram.I_j[j])) @@ -648,8 +648,7 @@ function GenerateDiagram!(diagram::InfluenceDiagram; default_probability::Bool=t if size(node.consequences) == Tuple((states[n] for n in diagram.I_j[j])) push!(diagram.Y, Consequences(j, node.consequences)) else - # TODO rephrase this error message - throw(DomainError("The dimensions of the consequences matrix of node $name should match the number of states its information set has. In this case ", Tuple((states[n] for n in (diagram.I_j[j]..., j))), ".")) + throw(DomainError("The dimensions of the consequences matrix should match the node's information states' cardinality. Expected $(Tuple((states[n] for n in diagram.I_j[j]))) for node $name, got $(size(node.consequences)).")) end end From 431d63218a945084b1b012633465ae02d77932d9 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 17 Aug 2021 16:26:17 +0300 Subject: [PATCH 014/133] Deleted the old node validation from the Generate Diagram function. --- src/influence_diagram.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 51c3bec4..c1747cd0 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -657,7 +657,6 @@ function GenerateDiagram!(diagram::InfluenceDiagram; default_probability::Bool=t diagram.S = States(states) # Validate influence diagram - validate_influence_diagram(diagram.S, diagram.C, diagram.D, diagram.V) sort!.((diagram.C, diagram.D, diagram.V, diagram.X, diagram.Y), by = x -> x.j) # Declare P and U if defaults are used From cefc17763226a0de8a4f4916623f5c348f78e2d7 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 17 Aug 2021 17:44:16 +0300 Subject: [PATCH 015/133] Implemented postive and negative utility translations in influence diagram struct. --- src/decision_model.jl | 67 +++++----------------------------------- src/influence_diagram.jl | 29 +++++++++-------- 2 files changed, 23 insertions(+), 73 deletions(-) diff --git a/src/decision_model.jl b/src/decision_model.jl index d32d9cf1..60d1c2c9 100644 --- a/src/decision_model.jl +++ b/src/decision_model.jl @@ -187,59 +187,6 @@ function lazy_probability_cut(model::Model, diagram::InfluenceDiagram, x_s::Path MOI.set(model, MOI.LazyConstraintCallback(), probability_cut) end -# --- Objective Functions --- - -""" - PositivePathUtility(S::States, U::AbstractPathUtility) - -Positive affine transformation of path utility. Always evaluates positive values. - -# Examples -```julia-repl -julia> U⁺ = PositivePathUtility(S, U) -julia> all(U⁺(s) > 0 for s in paths(S)) -true -``` -""" -struct PositivePathUtility <: AbstractPathUtility - U::AbstractPathUtility - min::Float64 - # TODO decide if P and U should be in struct InfluenceDiagram, if so then change this - # to outer construction function and make it update diagram.U - function PositivePathUtility(S::States, U::AbstractPathUtility) - u_min = minimum(U(s) for s in paths(S)) - new(U, u_min) - end -end - -(U::PositivePathUtility)(s::Path) = U.U(s) - U.min + 1 - -""" - NegativePathUtility(S::States, U::AbstractPathUtility) - -Negative affine transformation of path utility. Always evaluates negative values. - -# Examples -```julia-repl -julia> U⁻ = NegativePathUtility(S, U) -julia> all(U⁻(s) < 0 for s in paths(S)) -true -``` -""" -struct NegativePathUtility <: AbstractPathUtility - U::AbstractPathUtility - max::Float64 - # TODO decide if P and U should be in struct InfluenceDiagram, if so then change this - # to outer construction function and make it update diagram.U - function NegativePathUtility(S::States, U::AbstractPathUtility) - u_max = maximum(U(s) for s in paths(S)) - new(U, u_max) - end -end - -(U::NegativePathUtility)(s::Path) = U.U(s) - U.max - 1 - - """ expected_value(model::Model, x_s::PathCompatibilityVariables, @@ -272,7 +219,7 @@ function expected_value(model::Model, throw(DomainError("The probability_scale_factor must be greater than 0.")) end - @expression(model, sum(diagram.P(s) * x * diagram.U(s) * probability_scale_factor for (s, x) in x_s)) + @expression(model, sum(diagram.P(s) * x * diagram.U(s, diagram.translation) * probability_scale_factor for (s, x) in x_s)) end """ @@ -304,8 +251,8 @@ CVaR = conditional_value_at_risk(model, x_s, U, P, α) CVaR = conditional_value_at_risk(model, x_s, U, P, α; probability_scale_factor = 10.0) ``` """ -# TODO decide if P and U should be in struct InfluenceDiagram, if so then update this function function conditional_value_at_risk(model::Model, + diagram::InfluenceDiagram, x_s::PathCompatibilityVariables{N}, U::AbstractPathUtility, P::AbstractPathProbability, @@ -324,7 +271,7 @@ function conditional_value_at_risk(model::Model, end # Pre-computed parameters - u = collect(Iterators.flatten(U(s) for s in keys(x_s))) + u = collect(Iterators.flatten(diagram.U(s, diagram.translation) for s in keys(x_s))) u_sorted = sort(u) u_min = u_sorted[1] u_max = u_sorted[end] @@ -338,7 +285,7 @@ function conditional_value_at_risk(model::Model, @constraint(model, η ≤ u_max) ρ′_s = Dict{Path{N}, VariableRef}() for (s, x) in x_s - u_s = U(s) + u_s = diagram.U(s, diagram.translation) λ = @variable(model, binary=true) λ′ = @variable(model, binary=true) ρ = @variable(model) @@ -352,14 +299,14 @@ function conditional_value_at_risk(model::Model, @constraint(model, ρ ≤ λ * probability_scale_factor) @constraint(model, ρ′ ≤ λ′* probability_scale_factor) @constraint(model, ρ ≤ ρ′) - @constraint(model, ρ′ ≤ x * P(s) * probability_scale_factor) - @constraint(model, (x * P(s) - (1 - λ))* probability_scale_factor ≤ ρ) + @constraint(model, ρ′ ≤ x * diagram.P(s) * probability_scale_factor) + @constraint(model, (x * diagram.P(s) - (1 - λ))* probability_scale_factor ≤ ρ) ρ′_s[s] = ρ′ end @constraint(model, sum(values(ρ′_s)) == α * probability_scale_factor) # Return CVaR as an expression - CVaR = @expression(model, sum(ρ_bar * U(s) for (s, ρ_bar) in ρ′_s) / α) + CVaR = @expression(model, sum(ρ_bar * diagram.U(s, diagram.translation) for (s, ρ_bar) in ρ′_s) / α) return CVaR end diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index c1747cd0..53c2abb3 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -389,7 +389,9 @@ function (U::DefaultPathUtility)(s::Path) sum(Y(s[v.I_j]) for (v, Y) in zip(U.V, U.Y)) end - +function (U::DefaultPathUtility)(s::Path, t::Float64) + U(s) + t +end # --- Influence diagram --- @@ -415,6 +417,7 @@ mutable struct InfluenceDiagram Y::Vector{Consequences} P::AbstractPathProbability U::AbstractPathUtility + translation::Float64 function InfluenceDiagram() new(Vector{NodeData}()) end @@ -514,7 +517,6 @@ function deduce_node_indices(Nodes::Vector{NodeData}) # Chance and decision nodes C_and_D = filter(x -> !isa(x, ValueNodeData), Nodes) n_CD = length(C_and_D) - # Value nodes V = filter(x -> isa(x, ValueNodeData), Nodes) n_V = length(V) @@ -543,21 +545,17 @@ function deduce_node_indices(Nodes::Vector{NodeData}) end end - # Declare vectors for results (final resting place InfluenceDiagram.Names and InfluenceDiagram.I_j) Names = Vector{Name}(undef, n_CD+n_V) I_js = Vector{Vector{Node}}(undef, n_CD+n_V) - # Declare helper collections indices = Dict{Name, Node}() indexed_nodes = Set{Name}() - # Declare index for indexing nodes and loop iteration counter k index = 1 k = 1 while index <= n_CD+n_V && k == index - # First index nodes C and D nodes if index <= n_CD for j in C_and_D @@ -566,24 +564,19 @@ function deduce_node_indices(Nodes::Vector{NodeData}) # Update helper collections push!(indices, j.name => index) push!(indexed_nodes, j.name) - # Update results Names[index] = j.name I_js[index] = map(x -> indices[x], j.I_j) - # Increase index index += 1 end end - # After indexing all C and D nodes, index value nodes else - for v in V # Update results Names[index] = v.name I_js[index] = map(x -> indices[x], v.I_j) - # Increase index index += 1 end @@ -597,7 +590,6 @@ function deduce_node_indices(Nodes::Vector{NodeData}) end end - if index != k throw(DomainError("The influence diagram should be acyclic.")) end @@ -606,7 +598,11 @@ function deduce_node_indices(Nodes::Vector{NodeData}) end -function GenerateDiagram!(diagram::InfluenceDiagram; default_probability::Bool=true, default_utility::Bool=true) +function GenerateDiagram!(diagram::InfluenceDiagram; + default_probability::Bool=true, + default_utility::Bool=true, + positive_path_utility::Bool=false, + negative_path_utility::Bool=false) # Number of nodes nodes = [(n.name for n in diagram.Nodes)...] @@ -665,6 +661,13 @@ function GenerateDiagram!(diagram::InfluenceDiagram; default_probability::Bool=t end if default_utility diagram.U = DefaultPathUtility(diagram.V, diagram.Y) + if positive_path_utility + diagram.translation = 1 - minimum(diagram.U(s) for s in paths(diagram.S)) + elseif negative_path_utility + diagram.translation = -1 - maximum(diagram.U(s) for s in paths(diagram.S)) + else + diagram.translation = 0 + end end end From 8f3e2990ebae4521cf30c0fe9fcaae8a8a58aeb7 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 17 Aug 2021 18:01:07 +0300 Subject: [PATCH 016/133] Pig breeding example now using positive path utility. --- examples/pig_breeding.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/pig_breeding.jl b/examples/pig_breeding.jl index 6dce7dce..6932af81 100644 --- a/examples/pig_breeding.jl +++ b/examples/pig_breeding.jl @@ -41,13 +41,12 @@ end # Selling price AddValueNode!(diagram, "SP", ["H$N"], [300.0, 1000.0]) -GenerateDiagram!(diagram) +GenerateDiagram!(diagram, positive_path_utility = true) @info("Creating the decision model.") -#U⁺ = PositivePathUtility(S, U) model = Model() z = DecisionVariables(model, diagram) -x_s = PathCompatibilityVariables(model, diagram, z, probability_cut = true) +x_s = PathCompatibilityVariables(model, diagram, z, probability_cut = false) EV = expected_value(model, diagram, x_s) @objective(model, Max, EV) From b27b45dcf6ddc16cc8951ae6e078a33b2861e05b Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Wed, 18 Aug 2021 13:08:15 +0300 Subject: [PATCH 017/133] some to-do comments. --- src/influence_diagram.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 53c2abb3..6b5af781 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -128,7 +128,7 @@ julia> S = States([(2, [1, 3]), (3, [2, 4, 5])]) States([2, 3, 2, 3, 3]) ``` """ -function States(states::Vector{Tuple{State, Vector{Node}}}) +function States(states::Vector{Tuple{State, Vector{Node}}}) # TODO should this just be gotten rid of? S_j = Vector{State}(undef, sum(length(j) for (_, j) in states)) for (s, j) in states S_j[j] .= s @@ -141,7 +141,7 @@ end function validate_influence_diagram(S::States, C::Vector{ChanceNode}, D::Vector{DecisionNode}, V::Vector{ValueNode}) Validate influence diagram. -""" +""" #TODO should this be gotten rid of? function validate_influence_diagram(S::States, C::Vector{ChanceNode}, D::Vector{DecisionNode}, V::Vector{ValueNode}) n = length(C) + length(D) # in validate_node_data From 4c62087055a8d1f639820963ff9fe605d4b5743f Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Wed, 18 Aug 2021 14:24:01 +0300 Subject: [PATCH 018/133] New implementation of deduce indices. --- src/influence_diagram.jl | 64 ++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 6b5af781..10b351b5 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -551,47 +551,41 @@ function deduce_node_indices(Nodes::Vector{NodeData}) # Declare helper collections indices = Dict{Name, Node}() indexed_nodes = Set{Name}() - # Declare index for indexing nodes and loop iteration counter k + # Declare index index = 1 - k = 1 - - while index <= n_CD+n_V && k == index - # First index nodes C and D nodes - if index <= n_CD - for j in C_and_D - # Give bindex if node does not already have an index and all of its I_j have an index - if j.name ∉ indexed_nodes && Set(j.I_j) ⊆ indexed_nodes - # Update helper collections - push!(indices, j.name => index) - push!(indexed_nodes, j.name) - # Update results - Names[index] = j.name - I_js[index] = map(x -> indices[x], j.I_j) - # Increase index - index += 1 - end - end - # After indexing all C and D nodes, index value nodes - else - for v in V - # Update results - Names[index] = v.name - I_js[index] = map(x -> indices[x], v.I_j) - # Increase index - index += 1 - end + + + while true + # Index nodes C and D that don't yet have indices but whose I_j have indices + new_nodes = filter(j -> (j.name ∉ indexed_nodes && Set(j.I_j) ⊆ indexed_nodes), C_and_D) + for j in new_nodes + # Update helper collections + push!(indices, j.name => index) + push!(indexed_nodes, j.name) + # Update results + Names[index] = j.name + I_js[index] = map(x -> indices[x], j.I_j) + # Increase index + index += 1 end - # If new nodes have not been indexed during this iteration, terminate while loop - if index <= k - k += 1 - else - k = index + # If no new nodes were indexed this iteration, terminate while loop + if isempty(new_nodes) + if index < n_CD + throw(DomainError("The influence diagram should be acyclic.")) + else + break + end end end - if index != k - throw(DomainError("The influence diagram should be acyclic.")) + # Index value nodes + for v in V + # Update results + Names[index] = v.name + I_js[index] = map(x -> indices[x], v.I_j) + # Increase index + index += 1 end return Names, I_js From 5fba91fe2f11665ecc9f67c770a430151ad62b85 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 19 Aug 2021 15:28:34 +0300 Subject: [PATCH 019/133] Moved primitive type Name definition to top, deleted type NodeData and all its sub-structs. Adjusted the datatypes in InfluenceDiagram accordingly. --- src/influence_diagram.jl | 71 ++++++---------------------------------- 1 file changed, 10 insertions(+), 61 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 10b351b5..e3af35b2 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -6,10 +6,17 @@ using Base.Iterators: product """ Node = Int -Primitive type for node. Alias for `Int`. +Primitive type for node index. Alias for `Int`. """ const Node = Int +""" + Name = String + +Primitive type for node names. Alias for `String`. +""" +const Name = String + """ abstract type AbstractNode end @@ -395,20 +402,11 @@ end # --- Influence diagram --- -""" - Name = String - -Primitive type for name of node. Alias for `String`. -""" -const Name = String - - -abstract type NodeData end mutable struct InfluenceDiagram - Nodes::Vector{NodeData} + Nodes::Vector{AbstractNode} Names::Vector{Name} - I_j::Vector{Vector{State}} + I_j::Vector{Vector{Node}} S::States C::Vector{ChanceNode} D::Vector{DecisionNode} @@ -458,58 +456,9 @@ function validate_node_data(diagram::InfluenceDiagram, end end -struct DecisionNodeData <: NodeData - name::Name - I_j::Vector{Name} - states::Vector{Name} - function DecisionNodeData(name::Name, I_j::Vector{Name}, states::Vector{Name}) - return new(name, I_j, states) - end -end -function AddDecisionNode!(diagram::InfluenceDiagram, - name::Name, - I_j::Vector{Name}, - states::Vector{Name}) - validate_node_data(diagram, name, I_j, states = states) - push!(diagram.Nodes, DecisionNodeData(name, I_j, states)) -end -struct ChanceNodeData{N} <: NodeData - name::Name - I_j::Vector{Name} - states::Vector{Name} - probabilities::Array{Float64, N} - function ChanceNodeData(name::Name, I_j::Vector{Name}, states::Vector{Name}, probabilities::Array{Float64, N}) where N - return new{N}(name, I_j, states, probabilities) - end -end - -function AddChanceNode!(diagram::InfluenceDiagram, - name::Name, I_j::Vector{Name}, - states::Vector{Name}, - probabilities::Array{Float64, N}) where N - validate_node_data(diagram, name, I_j, states = states) - push!(diagram.Nodes, ChanceNodeData(name, I_j, states, probabilities)) -end - -struct ValueNodeData{N} <: NodeData - name::Name - I_j::Vector{Name} - consequences::Array{Float64, N} - function ValueNodeData(name::Name, I_j::Vector{Name}, consequences::Array{Float64, N}) where N - return new{N}(name, I_j, consequences) - end -end - -function AddValueNode!(diagram::InfluenceDiagram, - name::Name, - I_j::Vector{Name}, - consequences::Array{Float64, N}) where N - validate_node_data(diagram, name, I_j, value_node = true) - push!(diagram.Nodes, ValueNodeData(name, I_j, Array{Float64}(consequences))) -end function deduce_node_indices(Nodes::Vector{NodeData}) From 1af4cb8d4b1f139e344c519722121c5ffaff1217 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 19 Aug 2021 15:29:53 +0300 Subject: [PATCH 020/133] Redefined ValueNode, DecisionNode and ChanceNode structs. Added function AddNode! and changed validate_node accordingly. Commented out old definitions of these structs. --- src/influence_diagram.jl | 59 +++++++++++++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index e3af35b2..a8af6614 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -24,6 +24,9 @@ Node type for directed, acyclic graph. """ abstract type AbstractNode end + + +#= function validate_node(j::Node, I_j::Vector{Node}) if !allunique(I_j) throw(DomainError("All information nodes should be unique.")) @@ -90,6 +93,38 @@ struct ValueNode <: AbstractNode end end + +=# + +struct ChanceNode <: AbstractNode + name::Name + I_j::Vector{Name} + states::Vector{Name} + function ChanceNode(name, I_j, states) + return new(name, I_j, states) + end + +end + +struct DecisionNode <: AbstractNode + name::Name + I_j::Vector{Name} + states::Vector{Name} + function DecisionNode(name, I_j, states) + return new(name, I_j, states) + end +end + +struct ValueNode <: NodeData + name::Name + I_j::Vector{Name} + function ValueNode(name, I_j) + return new(name, I_j, consequences) + end +end + + + """ const State = Int @@ -143,7 +178,7 @@ function States(states::Vector{Tuple{State, Vector{Node}}}) # TODO should this j States(S_j) end - +#= """ function validate_influence_diagram(S::States, C::Vector{ChanceNode}, D::Vector{DecisionNode}, V::Vector{ValueNode}) @@ -186,7 +221,7 @@ function validate_influence_diagram(S::States, C::Vector{ChanceNode}, D::Vector{ end end end - +=# # --- Paths --- @@ -408,9 +443,9 @@ mutable struct InfluenceDiagram Names::Vector{Name} I_j::Vector{Vector{Node}} S::States - C::Vector{ChanceNode} - D::Vector{DecisionNode} - V::Vector{ValueNode} + C::Vector{Node} + D::Vector{Node} + V::Vector{Node} X::Vector{Probabilities} Y::Vector{Consequences} P::AbstractPathProbability @@ -423,10 +458,7 @@ end - -# --- Node raw data --- - -function validate_node_data(diagram::InfluenceDiagram, +function validate_node(diagram::InfluenceDiagram, name::Name, I_j::Vector{Name}; value_node::Bool=false, @@ -456,7 +488,14 @@ function validate_node_data(diagram::InfluenceDiagram, end end - +function AddNode!(diagram::InfluenceDiagram, node::AbstractNode) + if !isa(node, ValueNode) + validate_node_data(diagram, node.name, node.I_j, states = states) + else + validate_node_data(diagram, node.name, node.I_j, value_node = true) + end + push!(diagram.Nodes, node) +end From 3edfa7e99e32f75276f628566c61813f86d41e04 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 19 Aug 2021 15:30:39 +0300 Subject: [PATCH 021/133] Adjusted DefaultPathProbability to new definition of C in InfluenceDiagram. --- src/influence_diagram.jl | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index a8af6614..5e5c47ef 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -353,12 +353,21 @@ P(s) ``` """ struct DefaultPathProbability <: AbstractPathProbability - C::Vector{ChanceNode} + C::Vector{Node} + I_j::Vector{Vector{Node}} X::Vector{Probabilities} + function DefaultPathProbability(C, I_j, X) + if length(C) == length(I_j) + new(C, I_j, X) + else + throw(DomainError("The number of chance nodes and information sets have to be equal.")) + end + end + end function (P::DefaultPathProbability)(s::Path) - prod(X(s[[c.I_j; c.j]]) for (c, X) in zip(P.C, P.X)) + prod(X(s[[I_j; j]]) for (j, I_j, X) in zip(P.C, P.I_j, P.X)) end From 8baf1b785d3d5b60ffb79423f557e6c135c8551f Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 19 Aug 2021 19:11:38 +0300 Subject: [PATCH 022/133] Updated the DefaultPathUtility. --- src/influence_diagram.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 5e5c47ef..51beb6c2 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -432,12 +432,12 @@ U(s) ``` """ struct DefaultPathUtility <: AbstractPathUtility - V::Vector{ValueNode} + v_I_j::Vector{Vector{Node}} Y::Vector{Consequences} end function (U::DefaultPathUtility)(s::Path) - sum(Y(s[v.I_j]) for (v, Y) in zip(U.V, U.Y)) + sum(Y(s[I_j]) for (I_j, Y) in zip(U.v_I_j, U.Y)) end function (U::DefaultPathUtility)(s::Path, t::Float64) From dfce6e051edf9bdc8bd1ac695d3a8736cecfd3eb Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 19 Aug 2021 20:49:19 +0300 Subject: [PATCH 023/133] Fixed small typos and mistakes here and there. --- src/influence_diagram.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 51beb6c2..269e0e41 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -115,11 +115,11 @@ struct DecisionNode <: AbstractNode end end -struct ValueNode <: NodeData +struct ValueNode <: AbstractNode name::Name I_j::Vector{Name} function ValueNode(name, I_j) - return new(name, I_j, consequences) + return new(name, I_j) end end @@ -360,7 +360,7 @@ struct DefaultPathProbability <: AbstractPathProbability if length(C) == length(I_j) new(C, I_j, X) else - throw(DomainError("The number of chance nodes and information sets have to be equal.")) + throw(DomainError("The number of chance nodes and information sets given to DefaultPathProbability should be equal.")) end end @@ -461,7 +461,7 @@ mutable struct InfluenceDiagram U::AbstractPathUtility translation::Float64 function InfluenceDiagram() - new(Vector{NodeData}()) + new(Vector{AbstractNode}()) end end @@ -499,9 +499,9 @@ end function AddNode!(diagram::InfluenceDiagram, node::AbstractNode) if !isa(node, ValueNode) - validate_node_data(diagram, node.name, node.I_j, states = states) + validate_node(diagram, node.name, node.I_j, states = node.states) else - validate_node_data(diagram, node.name, node.I_j, value_node = true) + validate_node(diagram, node.name, node.I_j, value_node = true) end push!(diagram.Nodes, node) end From 4d45df1d1b856c7ee22934ea7ffa7dbcc68259cd Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 20 Aug 2021 09:48:07 +0300 Subject: [PATCH 024/133] Changed deduce_node_indices to GenerateArcs! and updated GenerateDiagram! to work with the new datatypes and structs. --- src/influence_diagram.jl | 78 ++++++++++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 269e0e41..0a8764d7 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -519,6 +519,7 @@ function deduce_node_indices(Nodes::Vector{NodeData}) n_V = length(V) +function validate_structure(Nodes::Vector{AbstractNode}, C_and_D::Vector{AbstractNode}, n_CD::Int, V::Vector{AbstractNode}, n_V::Int) # Validating node structure if n_CD == 0 throw(DomainError("The influence diagram must have chance or decision nodes.")) @@ -541,10 +542,28 @@ function deduce_node_indices(Nodes::Vector{NodeData}) @warn("Node $i is redundant.") end end +end + + +function GenerateArcs!(diagram::InfluenceDiagram) + + # Chance and decision nodes + C_and_D = filter(x -> !isa(x, ValueNode), diagram.Nodes) + n_CD = length(C_and_D) + # Value nodes + V_nodes = filter(x -> isa(x, ValueNode), diagram.Nodes) + n_V = length(V_nodes) + + validate_structure(diagram.Nodes, C_and_D, n_CD, V_nodes, n_V) # Declare vectors for results (final resting place InfluenceDiagram.Names and InfluenceDiagram.I_j) Names = Vector{Name}(undef, n_CD+n_V) - I_js = Vector{Vector{Node}}(undef, n_CD+n_V) + I_j = Vector{Vector{Node}}(undef, n_CD+n_V) + states = Vector{State}() + C = Vector{Node}() + D = Vector{Node}() + V = Vector{Node}() + # Declare helper collections indices = Dict{Name, Node}() indexed_nodes = Set{Name}() @@ -560,8 +579,14 @@ function deduce_node_indices(Nodes::Vector{NodeData}) push!(indices, j.name => index) push!(indexed_nodes, j.name) # Update results - Names[index] = j.name - I_js[index] = map(x -> indices[x], j.I_j) + Names[index] = Name(j.name) #TODO datatype conversion happens here + I_j[index] = map(x -> Node(indices[x]), j.I_j) + push!(states, State(length(j.states))) + if isa(j, ChanceNode) + push!(C, Node(index)) + else + push!(D, Node(index)) + end # Increase index index += 1 end @@ -576,16 +601,26 @@ function deduce_node_indices(Nodes::Vector{NodeData}) end end + # Index value nodes - for v in V + for v in V_nodes # Update results - Names[index] = v.name - I_js[index] = map(x -> indices[x], v.I_j) + Names[index] = Name(v.name) + I_j[index] = map(x -> Node(indices[x]), v.I_j) + push!(V, Node(index)) # Increase index index += 1 end - return Names, I_js + diagram.Names = Names + diagram.I_j = I_j + diagram.S = States(states) + diagram.C = C + diagram.D = D + diagram.V = V + # Declaring X and Y + diagram.X = Vector{Probabilities}() + diagram.Y = Vector{Consequences}() end @@ -595,22 +630,14 @@ function GenerateDiagram!(diagram::InfluenceDiagram; positive_path_utility::Bool=false, negative_path_utility::Bool=false) - # Number of nodes - nodes = [(n.name for n in diagram.Nodes)...] - n = length(nodes) - # Deduce indices for nodes - diagram.Names, diagram.I_j = deduce_node_indices(diagram.Nodes) + #diagram.Names, diagram.I_j, diagram.S, diagram.C, diagram.D, diagram.V = deduce_node_indices(diagram.Nodes) - # Declare states, C, D, V, X, Y - states = Vector{State}() - diagram.C = Vector{ChanceNode}() - diagram.D = Vector{DecisionNode}() - diagram.V = Vector{ValueNode}() - diagram.X = Vector{Probabilities}() - diagram.Y = Vector{Consequences}() + # Declare states, X, Y + #diagram.X = Vector{Probabilities}() + #diagram.Y = Vector{Consequences}() - # Fill states, C, D, V, X, Y +#= # Fill X, Y for (j, name) in enumerate(diagram.Names) node = diagram.Nodes[findfirst(x -> x.name == diagram.Names[j], diagram.Nodes)] @@ -640,18 +667,17 @@ function GenerateDiagram!(diagram::InfluenceDiagram; end end - # TODO ask Olli about this, if the States should be changed - diagram.S = States(states) - +=# # Validate influence diagram - sort!.((diagram.C, diagram.D, diagram.V, diagram.X, diagram.Y), by = x -> x.j) + sort!.((diagram.C, diagram.D, diagram.V)) + sort!.((diagram.X, diagram.Y), by = x -> x.j) # Declare P and U if defaults are used if default_probability - diagram.P = DefaultPathProbability(diagram.C, diagram.X) + diagram.P = DefaultPathProbability(diagram.C, diagram.I_j[diagram.C], diagram.X) end if default_utility - diagram.U = DefaultPathUtility(diagram.V, diagram.Y) + diagram.U = DefaultPathUtility(diagram.I_j[diagram.V], diagram.Y) if positive_path_utility diagram.translation = 1 - minimum(diagram.U(s) for s in paths(diagram.S)) elseif negative_path_utility From 3ced1d35f2e8aaf143047a9719c6633f94546b9f Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 20 Aug 2021 09:49:20 +0300 Subject: [PATCH 025/133] Added temporary AddProbabilities! and AddConsequences! functions until we decide how we want to implement these. --- src/influence_diagram.jl | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 0a8764d7..c3a4f9be 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -507,17 +507,25 @@ function AddNode!(diagram::InfluenceDiagram, node::AbstractNode) end +function AddProbabilities!(diagram::InfluenceDiagram, name::Name, probabilities::Array{Float64, N}) where N + j = findfirst(x -> x==name, diagram.Names) + if size(probabilities) == Tuple((diagram.S[n] for n in (diagram.I_j[j]..., j))) + push!(diagram.X, Probabilities(j, probabilities)) + else + throw(DomainError("The dimensions of a probability matrix should match the node's states' and information states' cardinality. Expected $(Tuple((diagram.S[n] for n in (diagram.I_j[j]..., j)))) for node $name, got $(size(probabilities)).")) + end +end -function deduce_node_indices(Nodes::Vector{NodeData}) - - # Chance and decision nodes - C_and_D = filter(x -> !isa(x, ValueNodeData), Nodes) - n_CD = length(C_and_D) - # Value nodes - V = filter(x -> isa(x, ValueNodeData), Nodes) - n_V = length(V) +function AddConsequences!(diagram::InfluenceDiagram, name::Name, consequences::Array{Float64, N}) where N + j = findfirst(x -> x==name, diagram.Names) + if size(consequences) == Tuple((diagram.S[n] for n in diagram.I_j[j])) + push!(diagram.Y, Consequences(j, consequences)) + else + throw(DomainError("The dimensions of the consequences matrix should match the node's information states' cardinality. Expected $(Tuple((diagram.S[n] for n in diagram.I_j[j]))) for node $name, got $(size(consequences)).")) + end +end function validate_structure(Nodes::Vector{AbstractNode}, C_and_D::Vector{AbstractNode}, n_CD::Int, V::Vector{AbstractNode}, n_V::Int) # Validating node structure From ecf3ff16aa157e8301f2fbc3559a7ff76e0fa547 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 20 Aug 2021 09:49:53 +0300 Subject: [PATCH 026/133] Updated DecisionProgramming.jl to include new functions and reorganized primitive types. --- src/DecisionProgramming.jl | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/DecisionProgramming.jl b/src/DecisionProgramming.jl index 578cf4f1..fa4b8f23 100644 --- a/src/DecisionProgramming.jl +++ b/src/DecisionProgramming.jl @@ -7,6 +7,7 @@ include("analysis.jl") include("printing.jl") export Node, + Name, AbstractNode, ChanceNode, DecisionNode, @@ -24,16 +25,12 @@ export Node, LocalDecisionStrategy, DecisionStrategy, validate_influence_diagram, - Name, InfluenceDiagram, + GenerateArcs!, GenerateDiagram!, - NodeData, - DecisionNodeData, - ChanceNodeData, - ValueNodeData, - AddDecisionNode!, - AddChanceNode!, - AddValueNode! + AddNode!, + AddProbabilities!, + AddConsequences! export DecisionVariables, PathCompatibilityVariables, From 362852a714fac415c7bf9dd91c92068e8ad77d77 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 20 Aug 2021 10:32:32 +0300 Subject: [PATCH 027/133] Updated DecisionVariables function to use new data types. --- src/decision_model.jl | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/decision_model.jl b/src/decision_model.jl index 60d1c2c9..dbab8d0e 100644 --- a/src/decision_model.jl +++ b/src/decision_model.jl @@ -1,21 +1,21 @@ using JuMP -function decision_variable(model::Model, S::States, d::DecisionNode, base_name::String="") +function decision_variable(model::Model, S::States, d::Node, I_d::Vector{Node}, base_name::String="") # Create decision variables. - dims = S[[d.I_j; d.j]] - z_j = Array{VariableRef}(undef, dims...) + dims = S[[I_d; d]] + z_d = Array{VariableRef}(undef, dims...) for s in paths(dims) - z_j[s...] = @variable(model, binary=true, base_name=base_name) + z_d[s...] = @variable(model, binary=true, base_name=base_name) end # Constraints to one decision per decision strategy. - for s_I in paths(S[d.I_j]) - @constraint(model, sum(z_j[s_I..., s_j] for s_j in 1:S[d.j]) == 1) + for s_I in paths(S[I_d]) + @constraint(model, sum(z_d[s_I..., s_d] for s_d in 1:S[d]) == 1) end - return z_j + return z_d end struct DecisionVariables - D::Vector{DecisionNode} + D::Vector{Node} z::Vector{<:Array{VariableRef}} end @@ -38,7 +38,7 @@ z = DecisionVariables(model, S, D) ``` """ function DecisionVariables(model::Model, diagram::InfluenceDiagram; names::Bool=false, name::String="z") - DecisionVariables(diagram.D, [decision_variable(model, diagram.S, d, (names ? "$(name)_$(d.j)$(s)" : "")) for d in diagram.D]) + DecisionVariables(diagram.D, [decision_variable(model, diagram.S, d, I_d, (names ? "$(name)_$(d.j)$(s)" : "")) for (d, I_d) in zip(diagram.D, diagram.I_j[diagram.D])]) end function is_forbidden(s::Path, forbidden_paths::Vector{ForbiddenPath}) From 98b7c84acee91ca9b574a07c50b4d419868b059c Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 20 Aug 2021 10:54:41 +0300 Subject: [PATCH 028/133] Updated PathCompatibilityVariables function to use new data types. --- src/decision_model.jl | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/decision_model.jl b/src/decision_model.jl index dbab8d0e..05fdb216 100644 --- a/src/decision_model.jl +++ b/src/decision_model.jl @@ -46,7 +46,7 @@ function is_forbidden(s::Path, forbidden_paths::Vector{ForbiddenPath}) end -function path_compatibility_variable(model::Model, z::DecisionVariables, base_name::String="") +function path_compatibility_variable(model::Model, base_name::String="") # Create a path compatiblity variable x = @variable(model, base_name=base_name) @@ -69,23 +69,23 @@ Base.iterate(x_s::PathCompatibilityVariables) = iterate(x_s.data) Base.iterate(x_s::PathCompatibilityVariables, i) = iterate(x_s.data, i) -function decision_strategy_constraint(model::Model, S::States, d::DecisionNode, D::Vector{DecisionNode}, z::Array{VariableRef}, x_s::PathCompatibilityVariables) +function decision_strategy_constraint(model::Model, S::States, d::Node, I_d::Vector{Node}, D::Vector{Node}, z::Array{VariableRef}, x_s::PathCompatibilityVariables) - # states of nodes in information structure (s_j | s_I(j)) - dims = S[[d.I_j; d.j]] + # states of nodes in information structure (s_d | s_I(d)) + dims = S[[I_d; d]] - # Theoretical upper bound based on number of paths with information structure (s_j | s_I(j)) divided by number of possible decision strategies in other decision nodes - other_decisions = map(d_n -> d_n.j, filter(d_n -> all(d_n.j != i for i in [d.I_j; d.j]), D)) + # Theoretical upper bound based on number of paths with information structure (s_d | s_I(d)) divided by number of possible decision strategies in other decision nodes + other_decisions = filter(j -> all(j != d_set for d_set in [I_d; d]), D) theoretical_ub = prod(S)/prod(dims)/ prod(S[other_decisions]) - # paths that have corresponding path compatibility variable + # paths that have a corresponding path compatibility variable existing_paths = keys(x_s) - for s_j_s_Ij in paths(dims) # iterate through all information states and states of d - # paths with (s_j | s_I(j)) information structure - feasible_paths = filter(s -> s[[d.I_j; d.j]] == s_j_s_Ij, existing_paths) + for s_d_s_Id in paths(dims) # iterate through all information states and states of d + # paths with (s_d | s_I(d)) information structure + feasible_paths = filter(s -> s[[I_d; d]] == s_d_s_Id, existing_paths) - @constraint(model, sum(get(x_s, s, 0) for s in feasible_paths) ≤ z[s_j_s_Ij...] * min(length(feasible_paths), theoretical_ub)) + @constraint(model, sum(get(x_s, s, 0) for s in feasible_paths) ≤ z[s_d_s_Id...] * min(length(feasible_paths), theoretical_ub)) end end @@ -138,7 +138,7 @@ function PathCompatibilityVariables(model::Model, # Create path compatibility variable for each effective path. N = length(diagram.S) variables_x_s = Dict{Path{N}, VariableRef}( - s => path_compatibility_variable(model, z, (names ? "$(name)$(s)" : "")) + s => path_compatibility_variable(model, (names ? "$(name)$(s)" : "")) for s in paths(diagram.S, fixed) if !iszero(diagram.P(s)) && !is_forbidden(s, forbidden_paths) ) @@ -147,7 +147,7 @@ function PathCompatibilityVariables(model::Model, # Add decision strategy constraints for each decision node for (d, z_d) in zip(z.D, z.z) - decision_strategy_constraint(model, diagram.S, d, z.D, z_d, x_s) + decision_strategy_constraint(model, diagram.S, d, diagram.I_j[d], z.D, z_d, x_s) end if probability_cut From 79abb6fb7fd4f8890c32dc3c41746bbc27fc87a4 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 20 Aug 2021 10:57:43 +0300 Subject: [PATCH 029/133] Deleted unnecessary variables from CVaR function --- src/decision_model.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/decision_model.jl b/src/decision_model.jl index 05fdb216..fd5e46cb 100644 --- a/src/decision_model.jl +++ b/src/decision_model.jl @@ -254,8 +254,6 @@ CVaR = conditional_value_at_risk(model, x_s, U, P, α; probability_scale_factor function conditional_value_at_risk(model::Model, diagram::InfluenceDiagram, x_s::PathCompatibilityVariables{N}, - U::AbstractPathUtility, - P::AbstractPathProbability, α::Float64; probability_scale_factor::Float64=1.0) where N From 6d4741d34a858df8df6ef17199351f851b6cd983 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 20 Aug 2021 12:38:36 +0300 Subject: [PATCH 030/133] Updated the DecisionStrategy and LocalDecisionStrategy structs to using new data types. --- src/decision_model.jl | 6 +++--- src/influence_diagram.jl | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/decision_model.jl b/src/decision_model.jl index fd5e46cb..3f15592a 100644 --- a/src/decision_model.jl +++ b/src/decision_model.jl @@ -316,8 +316,8 @@ end Construct decision strategy from variable refs. """ -function LocalDecisionStrategy(j::Node, z::Array{VariableRef}) - LocalDecisionStrategy(j, @. Int(round(value(z)))) +function LocalDecisionStrategy(d::Node, z::Array{VariableRef}) + LocalDecisionStrategy(d, @. Int(round(value(z)))) end """ @@ -331,5 +331,5 @@ Z = DecisionStrategy(z) ``` """ function DecisionStrategy(z::DecisionVariables) - DecisionStrategy(z.D, [LocalDecisionStrategy(d.j, v) for (d, v) in zip(z.D, z.z)]) + DecisionStrategy(z.D, [LocalDecisionStrategy(d, z_var) for (d, z_var) in zip(z.D, z.z)]) end diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index c3a4f9be..7109e5c2 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -711,9 +711,9 @@ Z(s_I) ``` """ struct LocalDecisionStrategy{N} <: AbstractArray{Int, N} - j::Node + d::Node data::Array{Int, N} - function LocalDecisionStrategy(j::Node, data::Array{Int, N}) where N + function LocalDecisionStrategy(d::Node, data::Array{Int, N}) where N if !all(0 ≤ x ≤ 1 for x in data) throw(DomainError("All values x must be 0 ≤ x ≤ 1.")) end @@ -722,7 +722,7 @@ struct LocalDecisionStrategy{N} <: AbstractArray{Int, N} throw(DomainError("Values should add to one.")) end end - new{N}(j, data) + new{N}(d, data) end end @@ -744,6 +744,6 @@ end Decision strategy type. """ struct DecisionStrategy - D::Vector{DecisionNode} - Z_j::Vector{LocalDecisionStrategy} + D::Vector{Node} + Z_d::Vector{LocalDecisionStrategy} end From 3d614d01e357042c6d36382962fb4ff822d5c853 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 20 Aug 2021 15:34:07 +0300 Subject: [PATCH 031/133] Added I_d to DecisionStrategy and DecisionVariables structs because it is needed in iterating compatible paths. --- src/decision_model.jl | 5 +++-- src/influence_diagram.jl | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/decision_model.jl b/src/decision_model.jl index 3f15592a..bd9f1a36 100644 --- a/src/decision_model.jl +++ b/src/decision_model.jl @@ -16,6 +16,7 @@ end struct DecisionVariables D::Vector{Node} + I_d::Vector{Vector{Node}} z::Vector{<:Array{VariableRef}} end @@ -38,7 +39,7 @@ z = DecisionVariables(model, S, D) ``` """ function DecisionVariables(model::Model, diagram::InfluenceDiagram; names::Bool=false, name::String="z") - DecisionVariables(diagram.D, [decision_variable(model, diagram.S, d, I_d, (names ? "$(name)_$(d.j)$(s)" : "")) for (d, I_d) in zip(diagram.D, diagram.I_j[diagram.D])]) + DecisionVariables(diagram.D, diagram.I_j[diagram.D], [decision_variable(model, diagram.S, d, I_d, (names ? "$(name)_$(d.j)$(s)" : "")) for (d, I_d) in zip(diagram.D, diagram.I_j[diagram.D])]) end function is_forbidden(s::Path, forbidden_paths::Vector{ForbiddenPath}) @@ -331,5 +332,5 @@ Z = DecisionStrategy(z) ``` """ function DecisionStrategy(z::DecisionVariables) - DecisionStrategy(z.D, [LocalDecisionStrategy(d, z_var) for (d, z_var) in zip(z.D, z.z)]) + DecisionStrategy(z.D, z.I_d, [LocalDecisionStrategy(d, z_var) for (d, z_var) in zip(z.D, z.z)]) end diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 7109e5c2..33229afe 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -745,5 +745,6 @@ Decision strategy type. """ struct DecisionStrategy D::Vector{Node} + I_d::Vector{Vector{Node}} Z_d::Vector{LocalDecisionStrategy} end From d2df6a762afc9f3c4c9b499f138505cbcd543e7d Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 20 Aug 2021 15:53:15 +0300 Subject: [PATCH 032/133] Updated CompatiblePaths to new data types --- src/analysis.jl | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/analysis.jl b/src/analysis.jl index bbb8a4e0..e41cd84e 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -1,12 +1,11 @@ struct CompatiblePaths S::States - C::Vector{ChanceNode} + C::Vector{Node} Z::DecisionStrategy fixed::Dict{Node, State} function CompatiblePaths(S, C, Z, fixed) - C_j = Set([c.j for c in C]) - if !all(k∈C_j for k in keys(fixed)) + if !all(k∈Set(C) for k in keys(fixed)) throw(DomainError("You can only fix chance states.")) end new(S, C, Z, fixed) @@ -30,48 +29,47 @@ end ``` """ -function CompatiblePaths(S::States, C::Vector{ChanceNode}, Z::DecisionStrategy) - CompatiblePaths(S, C, Z, Dict{Node, State}()) +function CompatiblePaths(diagram::InfluenceDiagram, Z::DecisionStrategy) + CompatiblePaths(diagram.S, diagram.C, Z, Dict{Node, State}()) end -function compatible_path(S::States, C::Vector{ChanceNode}, Z::DecisionStrategy, s_C::Path) +function compatible_path(S::States, C::Vector{Node}, Z::DecisionStrategy, s_C::Path) s = Array{Int}(undef, length(S)) for (c, s_C_j) in zip(C, s_C) - s[c.j] = s_C_j + s[c] = s_C_j end - for (d, Z_j) in zip(Z.D, Z.Z_j) - s[d.j] = Z_j((s[d.I_j]...,)) + for (d, I_d, Z_d) in zip(Z.D, Z.I_d, Z.Z_d) + s[d] = Z_d((s[I_d]...,)) end return (s...,) end -function Base.iterate(a::CompatiblePaths) - C_j = [c.j for c in a.C] - if isempty(a.fixed) - iter = paths(a.S[C_j]) +function Base.iterate(S_Z::CompatiblePaths) + if isempty(S_Z.fixed) + iter = paths(S_Z.S[S_Z.C]) else - ks = sort(collect(keys(a.fixed))) - fixed = Dict{Int, Int}(i => a.fixed[k] for (i, k) in enumerate(ks)) - iter = paths(a.S[C_j], fixed) + ks = sort(collect(keys(S_Z.fixed))) + fixed = Dict{Int, Int}(i => S_Z.fixed[k] for (i, k) in enumerate(ks)) + iter = paths(S_Z.S[S_Z.C], fixed) end next = iterate(iter) if next !== nothing s_C, state = next - return (compatible_path(a.S, a.C, a.Z, s_C), (iter, state)) + return (compatible_path(S_Z.S, S_Z.C, S_Z.Z, s_C), (iter, state)) end end -function Base.iterate(a::CompatiblePaths, gen) +function Base.iterate(S_Z::CompatiblePaths, gen) iter, state = gen next = iterate(iter, state) if next !== nothing s_C, state = next - return (compatible_path(a.S, a.C, a.Z, s_C), (iter, state)) + return (compatible_path(S_Z.S, S_Z.C, S_Z.Z, s_C), (iter, state)) end end Base.eltype(::Type{CompatiblePaths}) = Path -Base.length(a::CompatiblePaths) = prod(a.S[c.j] for c in a.C) +Base.length(S_Z::CompatiblePaths) = prod(S_Z.S[c] for c in S_Z.C) """ UtilityDistribution @@ -96,7 +94,7 @@ UtilityDistribution(S, P, U, Z) """ function UtilityDistribution(S::States, P::AbstractPathProbability, U::AbstractPathUtility, Z::DecisionStrategy) # Extract utilities and probabilities of active paths - S_Z = CompatiblePaths(S, P.C, Z) + S_Z = CompatiblePaths(diagram, Z) utilities = Vector{Float64}(undef, length(S_Z)) probabilities = Vector{Float64}(undef, length(S_Z)) for (i, s) in enumerate(S_Z) From 0564a16bdcedb4c27f3bd1de6d487c19f2427dc0 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 20 Aug 2021 16:58:37 +0300 Subject: [PATCH 033/133] Updated UtilityDistribution to new data types. --- src/analysis.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/analysis.jl b/src/analysis.jl index e41cd84e..b74c6e26 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -92,14 +92,14 @@ Constructs the probability mass function for path utilities on paths that are co UtilityDistribution(S, P, U, Z) ``` """ -function UtilityDistribution(S::States, P::AbstractPathProbability, U::AbstractPathUtility, Z::DecisionStrategy) +function UtilityDistribution(diagram::InfluenceDiagram, Z::DecisionStrategy) # Extract utilities and probabilities of active paths S_Z = CompatiblePaths(diagram, Z) utilities = Vector{Float64}(undef, length(S_Z)) probabilities = Vector{Float64}(undef, length(S_Z)) for (i, s) in enumerate(S_Z) - utilities[i] = U(s) - probabilities[i] = P(s) + utilities[i] = diagram.U(s) + probabilities[i] = diagram.P(s) end # Filter zero probabilities From a25dc00448f3da9bffa1a61af66dab4ced23939f Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Mon, 23 Aug 2021 10:10:51 +0300 Subject: [PATCH 034/133] Fixed parameters in CompatiblePaths inner construction function to new data types. --- src/analysis.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/analysis.jl b/src/analysis.jl index b74c6e26..3227d3c4 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -4,11 +4,11 @@ struct CompatiblePaths C::Vector{Node} Z::DecisionStrategy fixed::Dict{Node, State} - function CompatiblePaths(S, C, Z, fixed) - if !all(k∈Set(C) for k in keys(fixed)) + function CompatiblePaths(diagram, Z, fixed) + if !all(k∈Set(diagram.C) for k in keys(fixed)) throw(DomainError("You can only fix chance states.")) end - new(S, C, Z, fixed) + new(diagram.S, diagram.C, Z, fixed) end end From 7da522292664945b59a7d9caff9e14d4867d50af Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Mon, 23 Aug 2021 10:11:44 +0300 Subject: [PATCH 035/133] Updated state probabilities functions to use new data types --- src/analysis.jl | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/analysis.jl b/src/analysis.jl index 3227d3c4..3a5ab732 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -142,7 +142,7 @@ end """ function StateProbabilities(S::States, P::AbstractPathProbability, Z::DecisionStrategy, node::Node, state::State, prev::StateProbabilities) -Associates each node with array of conditional probabilities for each of its states occuring in active paths given fixed states and prior probability. +Associates each node with array of conditional probabilities for each of its states occuring in compatible paths given fixed states and prior probability. # Examples ```julia @@ -155,13 +155,13 @@ state = 2 StateProbabilities(S, P, Z, node, state, prev) ``` """ -function StateProbabilities(S::States, P::AbstractPathProbability, Z::DecisionStrategy, node::Node, state::State, prev::StateProbabilities) - prior = prev.probs[node][state] - fixed = prev.fixed +function StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy, node::Node, state::State, prior_probabilities::StateProbabilities) + prior = prior_probabilities.probs[node][state] + fixed = prior_probabilities.fixed push!(fixed, node => state) - probs = Dict(i => zeros(S[i]) for i in 1:length(S)) - for s in CompatiblePaths(S, P.C, Z, fixed), i in 1:length(S) - probs[i][s[i]] += P(s) / prior + probs = Dict(i => zeros(diagram.S[i]) for i in 1:length(diagram.S)) + for s in CompatiblePaths(diagram, Z, fixed), i in 1:length(diagram.S) + probs[i][s[i]] += diagram.P(s) / prior #TODO check what is this probability update. Does it really do Bayesian? end StateProbabilities(probs, fixed) end @@ -169,17 +169,17 @@ end """ function StateProbabilities(S::States, P::AbstractPathProbability, Z::DecisionStrategy) -Associates each node with array of probabilities for each of its states occuring in active paths. +Associates each node with array of probabilities for each of its states occuring in compatible paths. # Examples ```julia StateProbabilities(S, P, Z) ``` """ -function StateProbabilities(S::States, P::AbstractPathProbability, Z::DecisionStrategy) - probs = Dict(i => zeros(S[i]) for i in 1:length(S)) - for s in CompatiblePaths(S, P.C, Z), i in 1:length(S) - probs[i][s[i]] += P(s) +function StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy) + probs = Dict(i => zeros(diagram.S[i]) for i in 1:length(diagram.S)) + for s in CompatiblePaths(diagram, Z), i in 1:length(diagram.S) + probs[i][s[i]] += diagram.P(s) end StateProbabilities(probs, Dict{Node, State}()) end From 53503c6d18ca8bc607f4a5cbcb6355581a074868 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Mon, 23 Aug 2021 10:13:10 +0300 Subject: [PATCH 036/133] Redefined the parameters of value at risk and conditional value at risk functions to use utility distrubtion instead of u and p vectors separately. --- src/analysis.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/analysis.jl b/src/analysis.jl index 3a5ab732..15c34324 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -189,10 +189,10 @@ end Value-at-risk. """ -function value_at_risk(u::Vector{Float64}, p::Vector{Float64}, α::Float64) +function value_at_risk(U_distribution::UtilityDistribution, α::Float64) @assert 0 ≤ α ≤ 1 "We should have 0 ≤ α ≤ 1." - i = sortperm(u) - u, p = u[i], p[i] + perm = sortperm(U_distribution.u) + u, p = U_distribution.u[perm], U_distribution.p[perm] index = findfirst(x -> x≥α, cumsum(p)) return if index === nothing; u[end] else u[index] end end @@ -202,12 +202,12 @@ end Conditional value-at-risk. """ -function conditional_value_at_risk(u::Vector{Float64}, p::Vector{Float64}, α::Float64) - x_α = value_at_risk(u, p, α) +function conditional_value_at_risk(U_distribution::UtilityDistribution, α::Float64) + x_α = value_at_risk(U_distribution, α) if iszero(α) return x_α else - tail = u .≤ x_α - return (sum(u[tail] .* p[tail]) - (sum(p[tail]) - α) * x_α) / α + tail = U_distribution.u .≤ x_α + return (sum(U_distribution.u[tail] .* U_distribution.p[tail]) - (sum(U_distribution.p[tail]) - α) * x_α) / α end end From 3d74cb95f1d2f153080fa35b16f277b6814d4772 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Mon, 23 Aug 2021 10:20:59 +0300 Subject: [PATCH 037/133] Rephrased comment. --- src/analysis.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/analysis.jl b/src/analysis.jl index 15c34324..1fad2600 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -161,7 +161,7 @@ function StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy, node push!(fixed, node => state) probs = Dict(i => zeros(diagram.S[i]) for i in 1:length(diagram.S)) for s in CompatiblePaths(diagram, Z, fixed), i in 1:length(diagram.S) - probs[i][s[i]] += diagram.P(s) / prior #TODO check what is this probability update. Does it really do Bayesian? + probs[i][s[i]] += diagram.P(s) / prior #TODO double check that this is correct end StateProbabilities(probs, fixed) end From a80fb341705ce8d82c954b7a43b4f69330eb108e Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 24 Aug 2021 09:49:43 +0300 Subject: [PATCH 038/133] Fixed mistake in parameters of CompatiblePaths. --- src/analysis.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/analysis.jl b/src/analysis.jl index 1fad2600..7998972c 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -30,7 +30,7 @@ end """ function CompatiblePaths(diagram::InfluenceDiagram, Z::DecisionStrategy) - CompatiblePaths(diagram.S, diagram.C, Z, Dict{Node, State}()) + CompatiblePaths(diagram, Z, Dict{Node, State}()) end function compatible_path(S::States, C::Vector{Node}, Z::DecisionStrategy, s_C::Path) From 646f05f6dc2bfb5cae5021691c8b48f0a2f5c6b2 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 24 Aug 2021 09:50:36 +0300 Subject: [PATCH 039/133] Added States to InfluenceDiagram for printing purposes. --- src/influence_diagram.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 33229afe..602d6140 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -451,6 +451,7 @@ mutable struct InfluenceDiagram Nodes::Vector{AbstractNode} Names::Vector{Name} I_j::Vector{Vector{Node}} + States::Vector{Vector{Name}} S::States C::Vector{Node} D::Vector{Node} @@ -567,6 +568,7 @@ function GenerateArcs!(diagram::InfluenceDiagram) # Declare vectors for results (final resting place InfluenceDiagram.Names and InfluenceDiagram.I_j) Names = Vector{Name}(undef, n_CD+n_V) I_j = Vector{Vector{Node}}(undef, n_CD+n_V) + State_names = Vector{Vector{Name}}() states = Vector{State}() C = Vector{Node}() D = Vector{Node}() @@ -587,8 +589,9 @@ function GenerateArcs!(diagram::InfluenceDiagram) push!(indices, j.name => index) push!(indexed_nodes, j.name) # Update results - Names[index] = Name(j.name) #TODO datatype conversion happens here + Names[index] = Name(j.name) #TODO datatype conversion happens here, should we use push! ? I_j[index] = map(x -> Node(indices[x]), j.I_j) + push!(State_names, j.states) push!(states, State(length(j.states))) if isa(j, ChanceNode) push!(C, Node(index)) @@ -622,6 +625,7 @@ function GenerateArcs!(diagram::InfluenceDiagram) diagram.Names = Names diagram.I_j = I_j + diagram.States = State_names diagram.S = States(states) diagram.C = C diagram.D = D From 6dbae1984dec83fd964d3f1fed034da2e28e3079 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 24 Aug 2021 11:08:30 +0300 Subject: [PATCH 040/133] Revamped printing decision strategy. --- src/printing.jl | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/printing.jl b/src/printing.jl index 2c20ac1c..9d17d641 100644 --- a/src/printing.jl +++ b/src/printing.jl @@ -11,13 +11,26 @@ Print decision strategy. print_decision_strategy(S, Z) ``` """ -function print_decision_strategy(diagram::InfluenceDiagram, Z::DecisionStrategy) - for (d, Z_j) in zip(Z.D, Z.Z_j) - a1 = vec(collect(paths(diagram.S[d.I_j]))) - a2 = [Z_j(s_I) for s_I in a1] - labels = fill("States", length(a1)) - df = DataFrame(labels = labels, a1 = a1, a2 = a2) - pretty_table(df, ["Nodes", "$((d.I_j...,))", "$(d.j)"]) +function print_decision_strategy(diagram::InfluenceDiagram, Z::DecisionStrategy, state_probabilities::StateProbabilities; show_incompatible_states = false) + probs = state_probabilities.probs + + for (d, I_d, Z_d) in zip(Z.D, Z.I_d, Z.Z_d) + s_I = vec(collect(paths(diagram.S[I_d]))) + s_d = [Z_d(s) for s in s_I] + + if !isempty(I_d) + informations_states = [join([String(diagram.States[i][s_i]) for (i, s_i) in zip(I_d, s)], ", ") for s in s_I] + decision_probs = [ceil(prod(probs[i][s1] for (i, s1) in zip(I_d, s))) for s in s_I] + decisions = collect(p == 0 ? "--" : diagram.States[d][s] for (s, p) in zip(s_d, decision_probs)) + df = DataFrame(informations_states = informations_states, decisions = decisions) + if !show_incompatible_states + filter!(row -> row.decisions != "--", df) + end + pretty_table(df, ["State(s) of $(join([diagram.Names[i] for i in I_d], ", "))", "Decision in $(diagram.Names[d])"], alignment=:l) + else + df = DataFrame(decisions = diagram.States[d][s_d]) + pretty_table(df, ["Decision in $(diagram.Names[d])"], alignment=:l) + end end end From 4459693deeb30e0345684b16abe8e08076a34da7 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 24 Aug 2021 14:47:19 +0300 Subject: [PATCH 041/133] Updated n monitoring to new interface but this one is NOT working at the moment. --- examples/n_monitoring.jl | 89 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/examples/n_monitoring.jl b/examples/n_monitoring.jl index 7ec29b3e..fc8161ea 100644 --- a/examples/n_monitoring.jl +++ b/examples/n_monitoring.jl @@ -4,6 +4,94 @@ using DecisionProgramming Random.seed!(13) +const N = 4 +const c_k = rand(N) +const b = 0.03 +fortification(k, a) = [c_k[k], 0][a] + +@info("Creating the influence diagram.") +diagram = InfluenceDiagram() + +AddNode!(diagram, ChanceNode("L", [], ["high", "low"])) + +for i in 1:N + AddNode!(diagram, ChanceNode("R$i", ["L"], ["high", "low"])) + AddNode!(diagram, DecisionNode("A$i", ["R$i"], ["yes", "no"])) +end + +AddNode!(diagram, ChanceNode("F", ["L", ["A$i" for i in 1:N]...], ["failure", "success"])) + +AddNode!(diagram, ValueNode("T", ["F", ["A$i" for i in 1:N]...])) + +GenerateArcs!(diagram) + +X_L = [rand(), 0] +X_L[2] = 1.0 - X_L[1] +AddProbabilities!(diagram, "L", X_L) + +for i in 1:N + x, y = rand(2) + X_R = zeros(2,2) + X_R[1, 1] = max(x, 1-x) + X_R[1, 2] = 1.0 - X_R[1, 1] + X_R[2, 2] = max(y, 1-y) + X_R[2, 1] = 1.0 - X_R[2, 2] + AddProbabilities!(diagram, "R$i", X_R) +end + +for i in [1] + x, y = rand(2) + X_F = zeros(2, [2 for i in 1:N]..., 2) + for s in paths([2 for i in 1:N]) + d = exp(b * sum(fortification(k, a) for (k, a) in enumerate(s))) + X_F[1, s..., 1] = max(x, 1-x) / d + X_F[1, s..., 2] = 1.0 - X_F[1, s..., 1] + X_F[2, s..., 1] = min(y, 1-y) / d + X_F[2, s..., 2] = 1.0 - X_F[2, s..., 1] + end + AddProbabilities!(diagram, "F", X_F) +end + +Y_T = zeros([2 for i in 1:N]..., 2) +for s in paths([2 for i in 1:N]) + cost = sum(-fortification(k, a) for (k, a) in enumerate(s)) + Y_T[s..., 1] = cost + 0 + Y_T[s..., 2] = cost + 100 +end +AddConsequences!(diagram, "T", Y_T) + +GenerateDiagram!(diagram)#, positive_path_utility=true) + + +model = Model() +z = DecisionVariables(model, diagram) +x_s = PathCompatibilityVariables(model, diagram, z)#, probability_cut = false) +EV = expected_value(model, diagram, x_s) +@objective(model, Max, EV) + +@info("Starting the optimization process.") +optimizer = optimizer_with_attributes( + () -> Gurobi.Optimizer(Gurobi.Env()), + "IntFeasTol" => 1e-9, +) +set_optimizer(model, optimizer) +optimize!(model) + +@info("Extracting results.") +Z = DecisionStrategy(z) +U_distribution = UtilityDistribution(diagram, Z) +state_probabilities = StateProbabilities(diagram, Z) + +@info("Printing decision strategy:") +print_decision_strategy(diagram, Z, state_probabilities) + +@info("Printing utility distribution.") +print_utility_distribution(U_distribution) + +@info("Printing statistics") +print_statistics(U_distribution) + +#= const N = 4 const L = [1] const R_k = [k + 1 for k in 1:N] @@ -127,3 +215,4 @@ print_utility_distribution(udist) @info("Printing statistics") print_statistics(udist) +=# From de8ea836ee36aa4bc42d30ec310f984093446ef5 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 24 Aug 2021 14:47:57 +0300 Subject: [PATCH 042/133] Updated pig breeding. --- examples/pig_breeding.jl | 46 +++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/examples/pig_breeding.jl b/examples/pig_breeding.jl index 6932af81..d772e5fd 100644 --- a/examples/pig_breeding.jl +++ b/examples/pig_breeding.jl @@ -7,8 +7,20 @@ const N = 4 @info("Creating the influence diagram.") diagram = InfluenceDiagram() -AddChanceNode!(diagram, "H1", Vector{Name}(), ["ill", "healthy"], [0.1, 0.9]) +AddNode!(diagram, ChanceNode("H1", [], ["ill", "healthy"])) +for i in 1:N-1 + # Testing result + AddNode!(diagram, ChanceNode("T$i", ["H$i"], ["positive", "negative"])) + # Decision to treat + AddNode!(diagram, DecisionNode("D$i", ["T$i"], ["treat", "pass"])) + # Cost of treatment + AddNode!(diagram, ValueNode("C$i", ["D$i"])) + # Health of next period + AddNode!(diagram, ChanceNode("H$(i+1)", ["H$(i)", "D$(i)"], ["ill", "healthy"])) +end +AddNode!(diagram, ValueNode("SP", ["H$N"])) +GenerateArcs!(diagram) # Declare proability matrix for health nodes X_H = zeros(2, 2, 2) X_H[2, 2, 1] = 0.2 @@ -27,22 +39,21 @@ X_T[1, 2] = 1.0 - X_T[1, 1] X_T[2, 2] = 0.9 X_T[2, 1] = 1.0 - X_T[2, 2] +AddProbabilities!(diagram, "H1", [0.1, 0.9]) for i in 1:N-1 # Testing result - AddChanceNode!(diagram, "T$i", ["H$i"], ["positive", "negative"], X_T) - # Decision to treat - AddDecisionNode!(diagram, "D$i", ["T$i"], ["treat", "pass"]) + AddProbabilities!(diagram, "T$i", X_T) # Cost of treatment - AddValueNode!(diagram, "C$i", ["D$i"], [-100.0, 0.0]) + AddConsequences!(diagram, "C$i", [-100.0, 0.0]) # Health of next period - AddChanceNode!(diagram, "H$(i+1)", ["H$(i)", "D$(i)"], ["ill", "healthy"], X_H) + AddProbabilities!(diagram, "H$(i+1)", X_H) end - # Selling price -AddValueNode!(diagram, "SP", ["H$N"], [300.0, 1000.0]) +AddConsequences!(diagram, "SP", [300.0, 1000.0]) GenerateDiagram!(diagram, positive_path_utility = true) + @info("Creating the decision model.") model = Model() z = DecisionVariables(model, diagram) @@ -60,9 +71,18 @@ optimize!(model) @info("Extracting results.") Z = DecisionStrategy(z) +S_probabilities = StateProbabilities(diagram, Z) +U_distribution = UtilityDistribution(diagram, Z) + @info("Printing decision strategy:") -print_decision_strategy(diagram, Z) +print_decision_strategy(diagram, Z, S_probabilities) + +@info("Printing utility distribution.") +print_utility_distribution(U_distribution) + +@info("Printing statistics") +print_statistics(U_distribution) #= @info("State probabilities:") @@ -80,11 +100,3 @@ for state in 1:2 print_state_probabilities(sprobs2, treat) end =# -@info("Computing utility distribution.") -udist = UtilityDistribution(diagram.S, diagram.P, diagram.U, Z) - -@info("Printing utility distribution.") -print_utility_distribution(udist) - -@info("Printing statistics") -print_statistics(udist) From aa415b963604ffe3fc226a82441b096c254a1b0f Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 24 Aug 2021 14:48:18 +0300 Subject: [PATCH 043/133] Updated used car buyer. --- examples/used_car_buyer.jl | 134 +++++++------------------------------ 1 file changed, 24 insertions(+), 110 deletions(-) diff --git a/examples/used_car_buyer.jl b/examples/used_car_buyer.jl index bf9c949b..5ceedd6c 100644 --- a/examples/used_car_buyer.jl +++ b/examples/used_car_buyer.jl @@ -6,126 +6,39 @@ using DecisionProgramming @info("Creating the influence diagram.") diagram = InfluenceDiagram() -AddChanceNode!(diagram, "O", Vector{Name}(), ["lemon", "peach"], [0.2, 0.8]) +AddNode!(diagram, ChanceNode("O", [], ["lemon", "peach"])) +AddNode!(diagram, ChanceNode("R", ["O", "T"], ["no test", "lemon", "peach"])) -X_R = zeros(2, 2, 3) -X_R[1, 1, :] = [1,0,0] -X_R[1, 2, :] = [0,1,0] -X_R[2, 1, :] = [1,0,0] -X_R[2, 2, :] = [0,0,1] -AddChanceNode!(diagram, "R", ["O", "T"], ["no test", "lemon", "peach"], X_R) -AddDecisionNode!(diagram, "T", Vector{Name}(), ["no test", "test"]) -AddDecisionNode!(diagram, "A", ["R"], ["buy without guarantee", "buy with guarantee", "don't buy"]) -AddValueNode!(diagram, "V1", ["T"], [0.0, -25.0]) -AddValueNode!(diagram, "V2", ["A"], [100.0, 40.0, 0.0]) -AddValueNode!(diagram, "V3", ["O", "A"], [-200.0 0.0 0.0; -40.0 -20.0 0.0]) +AddNode!(diagram, DecisionNode("T", [], ["no test", "test"])) +AddNode!(diagram, DecisionNode("A", ["R"], ["buy without guarantee", "buy with guarantee", "don't buy"])) -GenerateDiagram!(diagram) +AddNode!(diagram, ValueNode("V1", ["T"])) +AddNode!(diagram, ValueNode("V2", ["A"])) +AddNode!(diagram, ValueNode("V3", ["O", "A"])) +GenerateArcs!(diagram) -@info("Creating the decision model.") -model = Model() -z = DecisionVariables(model, diagram) -x_s = PathCompatibilityVariables(model, diagram, z) -EV = expected_value(model, diagram, x_s) -@objective(model, Max, EV) - -@info("Starting the optimization process.") -optimizer = optimizer_with_attributes( - () -> Gurobi.Optimizer(Gurobi.Env()), - "IntFeasTol" => 1e-9, -) -set_optimizer(model, optimizer) -optimize!(model) -@info("Extracting results.") -Z = DecisionStrategy(z) - -@info("Printing decision strategy:") -print_decision_strategy(diagram, Z) - -@info("Computing utility distribution.") -udist = UtilityDistribution(diagram.S, diagram.P, diagram.U, Z) - -@info("Printing utility distribution.") -print_utility_distribution(udist) - -@info("Printing expected utility.") -print_statistics(udist) - - -#= - -const O = 1 # Chance node: lemon or peach -const T = 2 # Decision node: pay stranger for advice -const R = 3 # Chance node: observation of state of the car -const A = 4 # Decision node: purchase alternative -const O_states = ["lemon", "peach"] -const T_states = ["no test", "test"] -const R_states = ["no test", "lemon", "peach"] -const A_states = ["buy without guarantee", "buy with guarantee", "don't buy"] - -@info("Creating the influence diagram.") -S = States([ - (length(O_states), [O]), - (length(T_states), [T]), - (length(R_states), [R]), - (length(A_states), [A]), -]) -C = Vector{ChanceNode}() -D = Vector{DecisionNode}() -V = Vector{ValueNode}() -X = Vector{Probabilities}() -Y = Vector{Consequences}() - - -I_O = Vector{Node}() -X_O = [0.2, 0.8] -push!(C, ChanceNode(O, I_O)) -push!(X, Probabilities(O, X_O)) - -I_T = Vector{Node}() -push!(D, DecisionNode(T, I_T)) - -I_R = [O, T] -X_R = zeros(S[O], S[T], S[R]) +X_R = zeros(2, 2, 3) X_R[1, 1, :] = [1,0,0] X_R[1, 2, :] = [0,1,0] X_R[2, 1, :] = [1,0,0] X_R[2, 2, :] = [0,0,1] -push!(C, ChanceNode(R, I_R)) -push!(X, Probabilities(R, X_R)) - -I_A = [R] -push!(D, DecisionNode(A, I_A)) - -I_V1 = [T] -Y_V1 = [0.0, -25.0] -push!(V, ValueNode(5, I_V1)) -push!(Y, Consequences(5, Y_V1)) +AddProbabilities!(diagram, "R", X_R) +AddProbabilities!(diagram, "O", [0.2, 0.8]) -I_V2 = [A] -Y_V2 = [100.0, 40.0, 0.0] -push!(V, ValueNode(6, I_V2)) -push!(Y, Consequences(6, Y_V2)) +AddConsequences!(diagram, "V1", [0.0, -25.0]) +AddConsequences!(diagram, "V2", [100.0, 40.0, 0.0]) +AddConsequences!(diagram, "V3", [-200.0 0.0 0.0; -40.0 -20.0 0.0]) -I_V3 = [O, A] -Y_V3 = [-200.0 0.0 0.0; - -40.0 -20.0 0.0] -push!(V, ValueNode(7, I_V3)) -push!(Y, Consequences(7, Y_V3)) - -validate_influence_diagram(S, C, D, V) -sort!.((C, D, V, X, Y), by = x -> x.j) +GenerateDiagram!(diagram) -P = DefaultPathProbability(C, X) -U = DefaultPathUtility(V, Y) @info("Creating the decision model.") model = Model() -z = DecisionVariables(model, S, D) -x_s = PathCompatibilityVariables(model, z, S, P) -EV = expected_value(model, x_s, U, P) +z = DecisionVariables(model, diagram) +x_s = PathCompatibilityVariables(model, diagram, z) +EV = expected_value(model, diagram, x_s) @objective(model, Max, EV) @info("Starting the optimization process.") @@ -138,16 +51,17 @@ optimize!(model) @info("Extracting results.") Z = DecisionStrategy(z) +S_probabilities = StateProbabilities(diagram, Z) +U_distribution = UtilityDistribution(diagram, Z) @info("Printing decision strategy:") -print_decision_strategy(S, Z) +print_decision_strategy(diagram, Z, S_probabilities) @info("Computing utility distribution.") -udist = UtilityDistribution(S, P, U, Z) +udist = UtilityDistribution(diagram, Z) @info("Printing utility distribution.") -print_utility_distribution(udist) +print_utility_distribution(U_distribution) @info("Printing expected utility.") -print_statistics(udist) -=# +print_statistics(U_distribution) From e718511c6b8c65b90fc912f44075a99aa9eaa8fb Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 24 Aug 2021 15:57:55 +0300 Subject: [PATCH 044/133] Fixed N-monitoring example. --- examples/n_monitoring.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/n_monitoring.jl b/examples/n_monitoring.jl index fc8161ea..20041bcc 100644 --- a/examples/n_monitoring.jl +++ b/examples/n_monitoring.jl @@ -55,8 +55,8 @@ end Y_T = zeros([2 for i in 1:N]..., 2) for s in paths([2 for i in 1:N]) cost = sum(-fortification(k, a) for (k, a) in enumerate(s)) - Y_T[s..., 1] = cost + 0 - Y_T[s..., 2] = cost + 100 + Y_T[1, s...] = cost + 0 + Y_T[2, s...] = cost + 100 end AddConsequences!(diagram, "T", Y_T) From 0423c3bf096376f52b2a7e14b57cb5581cfba301 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Wed, 25 Aug 2021 10:33:12 +0300 Subject: [PATCH 045/133] Deleted commented out code including old data types and node and influence diagram validation functions. --- src/influence_diagram.jl | 115 --------------------------------------- 1 file changed, 115 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 602d6140..97cf9c1a 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -25,77 +25,6 @@ Node type for directed, acyclic graph. abstract type AbstractNode end - -#= -function validate_node(j::Node, I_j::Vector{Node}) - if !allunique(I_j) - throw(DomainError("All information nodes should be unique.")) - end - if !all(i < j for i in I_j) - throw(DomainError("All nodes in the information set must be less than node j.")) - end -end - -""" - ChanceNode <: AbstractNode - -Chance node type. - -# Examples -```julia -c = ChanceNode(3, [1, 2]) -``` -""" -struct ChanceNode <: AbstractNode - j::Node - I_j::Vector{Node} - function ChanceNode(j::Node, I_j::Vector{Node}) - validate_node(j, I_j) - new(j, I_j) - end -end - -""" - DecisionNode <: AbstractNode - -Decision node type. - -# Examples -```julia -d = DecisionNode(2, [1]) -``` -""" -struct DecisionNode <: AbstractNode - j::Node - I_j::Vector{Node} - function DecisionNode(j::Node, I_j::Vector{Node}) - validate_node(j, I_j) - new(j, I_j) - end -end - -""" - ValueNode <: AbstractNode - -Value node type. - -# Examples -```julia -v = ValueNode(4, [1, 3]) -``` -""" -struct ValueNode <: AbstractNode - j::Node - I_j::Vector{Node} - function ValueNode(j::Node, I_j::Vector{Node}) - validate_node(j, I_j) - new(j, I_j) - end -end - - -=# - struct ChanceNode <: AbstractNode name::Name I_j::Vector{Name} @@ -178,50 +107,6 @@ function States(states::Vector{Tuple{State, Vector{Node}}}) # TODO should this j States(S_j) end -#= -""" - function validate_influence_diagram(S::States, C::Vector{ChanceNode}, D::Vector{DecisionNode}, V::Vector{ValueNode}) - -Validate influence diagram. -""" #TODO should this be gotten rid of? -function validate_influence_diagram(S::States, C::Vector{ChanceNode}, D::Vector{DecisionNode}, V::Vector{ValueNode}) - n = length(C) + length(D) - # in validate_node_data - if length(S) != n - throw(DomainError("Each change and decision node should have states.")) - end - - # in deduce_node_indices logic... - if Set(c.j for c in C) ∪ Set(d.j for d in D) != Set(1:n) - throw(DomainError("Union of change and decision nodes should be {1,...,n}.")) - end - - # in deduce_node_indices logic... - if Set(v.j for v in V) != Set((n+1):(n+length(V))) - throw(DomainError("Values nodes should be {n+1,...,n+|V|}.")) - end - - # in deduce_node_indices - I_V = union((v.I_j for v in V)...) - if !(I_V ⊆ Set(1:n)) - throw(DomainError("Each information set I(v) for value node v should be a subset of C∪D.")) - end - # in deduce_node_indices - # Check for redundant nodes. - leaf_nodes = setdiff(1:n, (c.I_j for c in C)..., (d.I_j for d in D)...) - for i in leaf_nodes - if !(i∈I_V) - @warn("Chance or decision node $i is redundant.") - end - end - # in validate_node_data - for v in V - if isempty(v.I_j) - @warn("Value node $(v.j) is redundant.") - end - end -end -=# # --- Paths --- From 1341082ac72bac299113ed39615b8e83c8c4e421 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Wed, 25 Aug 2021 10:39:39 +0300 Subject: [PATCH 046/133] Chanced all references to chance node use letter c instead of j. --- src/influence_diagram.jl | 67 +++++++++------------------------------- 1 file changed, 15 insertions(+), 52 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 97cf9c1a..dcab4a7e 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -186,15 +186,15 @@ julia> X(s) ``` """ struct Probabilities{N} <: AbstractArray{Float64, N} - j::Node + c::Node data::Array{Float64, N} - function Probabilities(j::Node, data::Array{Float64, N}) where N + function Probabilities(c::Node, data::Array{Float64, N}) where N for i in CartesianIndices(size(data)[1:end-1]) if !(sum(data[i, :]) ≈ 1) throw(DomainError("Probabilities should sum to one.")) end end - new{N}(j, data) + new{N}(c, data) end end @@ -239,11 +239,11 @@ P(s) """ struct DefaultPathProbability <: AbstractPathProbability C::Vector{Node} - I_j::Vector{Vector{Node}} + I_c::Vector{Vector{Node}} X::Vector{Probabilities} - function DefaultPathProbability(C, I_j, X) - if length(C) == length(I_j) - new(C, I_j, X) + function DefaultPathProbability(C, I_c, X) + if length(C) == length(I_c) + new(C, I_c, X) else throw(DomainError("The number of chance nodes and information sets given to DefaultPathProbability should be equal.")) end @@ -252,7 +252,7 @@ struct DefaultPathProbability <: AbstractPathProbability end function (P::DefaultPathProbability)(s::Path) - prod(X(s[[I_j; j]]) for (j, I_j, X) in zip(P.C, P.I_j, P.X)) + prod(X(s[[I_c; c]]) for (c, I_c, X) in zip(P.C, P.I_c, P.X)) end @@ -394,12 +394,12 @@ end function AddProbabilities!(diagram::InfluenceDiagram, name::Name, probabilities::Array{Float64, N}) where N - j = findfirst(x -> x==name, diagram.Names) + c = findfirst(x -> x==name, diagram.Names) - if size(probabilities) == Tuple((diagram.S[n] for n in (diagram.I_j[j]..., j))) - push!(diagram.X, Probabilities(j, probabilities)) + if size(probabilities) == Tuple((diagram.S[n] for n in (diagram.I_j[c]..., c))) + push!(diagram.X, Probabilities(c, probabilities)) else - throw(DomainError("The dimensions of a probability matrix should match the node's states' and information states' cardinality. Expected $(Tuple((diagram.S[n] for n in (diagram.I_j[j]..., j)))) for node $name, got $(size(probabilities)).")) + throw(DomainError("The dimensions of a probability matrix should match the node's states' and information states' cardinality. Expected $(Tuple((diagram.S[n] for n in (diagram.I_j[c]..., c)))) for node $name, got $(size(probabilities)).")) end end @@ -527,47 +527,10 @@ function GenerateDiagram!(diagram::InfluenceDiagram; positive_path_utility::Bool=false, negative_path_utility::Bool=false) - # Deduce indices for nodes - #diagram.Names, diagram.I_j, diagram.S, diagram.C, diagram.D, diagram.V = deduce_node_indices(diagram.Nodes) - - # Declare states, X, Y - #diagram.X = Vector{Probabilities}() - #diagram.Y = Vector{Consequences}() - -#= # Fill X, Y - for (j, name) in enumerate(diagram.Names) - node = diagram.Nodes[findfirst(x -> x.name == diagram.Names[j], diagram.Nodes)] - - if isa(node, DecisionNodeData) - push!(states, length(node.states)) - push!(diagram.D, DecisionNode(j, diagram.I_j[j])) - - elseif isa(node, ChanceNodeData) - push!(states, length(node.states)) - push!(diagram.C, ChanceNode(j, diagram.I_j[j])) - - # Check dimensions of probabiltiies match states of (I_j, j) - if size(node.probabilities) == Tuple((states[n] for n in (diagram.I_j[j]..., j))) - push!(diagram.X, Probabilities(j, node.probabilities)) - else - throw(DomainError("The dimensions of a probability matrix should match the node's states' and information states' cardinality. Expected $(Tuple((states[n] for n in (diagram.I_j[j]..., j)))) for node $name, got $(size(node.probabilities)).")) - end - elseif isa(node, ValueNodeData) - push!(diagram.V, ValueNode(j, diagram.I_j[j])) - - # Check dimensions of consequences match states of I_j - if size(node.consequences) == Tuple((states[n] for n in diagram.I_j[j])) - push!(diagram.Y, Consequences(j, node.consequences)) - else - throw(DomainError("The dimensions of the consequences matrix should match the node's information states' cardinality. Expected $(Tuple((states[n] for n in diagram.I_j[j]))) for node $name, got $(size(node.consequences)).")) - end - - end - end -=# # Validate influence diagram - sort!.((diagram.C, diagram.D, diagram.V)) - sort!.((diagram.X, diagram.Y), by = x -> x.j) + sort!(diagram.X, by = x -> x.c) + sort!(diagram.Y, by = x -> x.j) + # Declare P and U if defaults are used if default_probability From bd0bb01afa0d9d7c8f419d6518f6f6c15a2882f5 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Wed, 25 Aug 2021 12:37:22 +0300 Subject: [PATCH 047/133] Chanced all references to value nodes use letter v instead of j. --- src/influence_diagram.jl | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index dcab4a7e..b5b903c9 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -273,7 +273,7 @@ julia> Y(s) ``` """ struct Consequences{N} <: AbstractArray{Float64, N} - j::Node + v::Node data::Array{Float64, N} end @@ -317,12 +317,12 @@ U(s) ``` """ struct DefaultPathUtility <: AbstractPathUtility - v_I_j::Vector{Vector{Node}} + I_v::Vector{Vector{Node}} Y::Vector{Consequences} end function (U::DefaultPathUtility)(s::Path) - sum(Y(s[I_j]) for (I_j, Y) in zip(U.v_I_j, U.Y)) + sum(Y(s[I_v]) for (I_v, Y) in zip(U.I_v, U.Y)) end function (U::DefaultPathUtility)(s::Path, t::Float64) @@ -358,6 +358,7 @@ function validate_node(diagram::InfluenceDiagram, I_j::Vector{Name}; value_node::Bool=false, states::Vector{Name}=Vector{Name}()) + if !allunique([map(x -> x.name, diagram.Nodes)..., name]) throw(DomainError("All node names should be unique.")) end @@ -404,12 +405,12 @@ function AddProbabilities!(diagram::InfluenceDiagram, name::Name, probabilities: end function AddConsequences!(diagram::InfluenceDiagram, name::Name, consequences::Array{Float64, N}) where N - j = findfirst(x -> x==name, diagram.Names) + v = findfirst(x -> x==name, diagram.Names) - if size(consequences) == Tuple((diagram.S[n] for n in diagram.I_j[j])) - push!(diagram.Y, Consequences(j, consequences)) + if size(consequences) == Tuple((diagram.S[n] for n in diagram.I_j[v])) + push!(diagram.Y, Consequences(v, consequences)) else - throw(DomainError("The dimensions of the consequences matrix should match the node's information states' cardinality. Expected $(Tuple((diagram.S[n] for n in diagram.I_j[j]))) for node $name, got $(size(consequences)).")) + throw(DomainError("The dimensions of the consequences matrix should match the node's information states' cardinality. Expected $(Tuple((diagram.S[n] for n in diagram.I_j[v]))) for node $name, got $(size(consequences)).")) end end @@ -529,7 +530,7 @@ function GenerateDiagram!(diagram::InfluenceDiagram; # Validate influence diagram sort!(diagram.X, by = x -> x.c) - sort!(diagram.Y, by = x -> x.j) + sort!(diagram.Y, by = x -> x.v) # Declare P and U if defaults are used From 3e66dd7103ee33676eaa330d161b8b94ecb3fb25 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Wed, 25 Aug 2021 12:39:13 +0300 Subject: [PATCH 048/133] Removed duplicate code. --- examples/used_car_buyer.jl | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/used_car_buyer.jl b/examples/used_car_buyer.jl index 5ceedd6c..804506f1 100644 --- a/examples/used_car_buyer.jl +++ b/examples/used_car_buyer.jl @@ -57,9 +57,6 @@ U_distribution = UtilityDistribution(diagram, Z) @info("Printing decision strategy:") print_decision_strategy(diagram, Z, S_probabilities) -@info("Computing utility distribution.") -udist = UtilityDistribution(diagram, Z) - @info("Printing utility distribution.") print_utility_distribution(U_distribution) From 6fb622a36ec32f923d19f17c72712729aace117b Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 26 Aug 2021 14:07:20 +0300 Subject: [PATCH 049/133] Renamed functions to abide Julia style guide. --- src/DecisionProgramming.jl | 18 ++++++++++-------- src/influence_diagram.jl | 7 ++++--- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/DecisionProgramming.jl b/src/DecisionProgramming.jl index fa4b8f23..2366ebac 100644 --- a/src/DecisionProgramming.jl +++ b/src/DecisionProgramming.jl @@ -26,18 +26,18 @@ export Node, DecisionStrategy, validate_influence_diagram, InfluenceDiagram, - GenerateArcs!, - GenerateDiagram!, - AddNode!, - AddProbabilities!, - AddConsequences! + generate_arcs!, + generate_diagram!, + add_node!, + ProbabilityMatrix, + set_probability!, + add_probabilities!, + add_consequences! export DecisionVariables, PathCompatibilityVariables, ForbiddenPath, lazy_probability_cut, - PositivePathUtility, - NegativePathUtility, expected_value, value_at_risk, conditional_value_at_risk @@ -46,7 +46,9 @@ export random_diagram export CompatiblePaths, UtilityDistribution, - StateProbabilities + StateProbabilities, + value_at_risk, + conditional_value_at_risk export print_decision_strategy, print_utility_distribution, diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index b5b903c9..79779ab8 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -352,6 +352,7 @@ mutable struct InfluenceDiagram end +# --- Adding nodes --- function validate_node(diagram::InfluenceDiagram, name::Name, @@ -384,7 +385,7 @@ function validate_node(diagram::InfluenceDiagram, end end -function AddNode!(diagram::InfluenceDiagram, node::AbstractNode) +function add_node!(diagram::InfluenceDiagram, node::AbstractNode) if !isa(node, ValueNode) validate_node(diagram, node.name, node.I_j, states = node.states) else @@ -440,7 +441,7 @@ function validate_structure(Nodes::Vector{AbstractNode}, C_and_D::Vector{Abstrac end -function GenerateArcs!(diagram::InfluenceDiagram) +function generate_arcs!(diagram::InfluenceDiagram) # Chance and decision nodes C_and_D = filter(x -> !isa(x, ValueNode), diagram.Nodes) @@ -522,7 +523,7 @@ function GenerateArcs!(diagram::InfluenceDiagram) end -function GenerateDiagram!(diagram::InfluenceDiagram; +function generate_diagram!(diagram::InfluenceDiagram; default_probability::Bool=true, default_utility::Bool=true, positive_path_utility::Bool=false, From b3f3a30241295977bdba4bb10238ca08c5c62e57 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 26 Aug 2021 14:10:32 +0300 Subject: [PATCH 050/133] Added the ProbabilityMatrix struct and set_probability function. Edited add_probability for struct. Added validations of probabilities in generate_diagram function. --- src/influence_diagram.jl | 86 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 79779ab8..3741c489 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -395,11 +395,84 @@ function add_node!(diagram::InfluenceDiagram, node::AbstractNode) end -function AddProbabilities!(diagram::InfluenceDiagram, name::Name, probabilities::Array{Float64, N}) where N +# --- Adding Probabilities --- + +struct ProbabilityMatrix{N} <: AbstractArray{Float64, N} + nodes::Vector{Name} + indices::Vector{Dict{Name, Int}} + matrix::Array{Float64, N} +end + +Base.size(PM::ProbabilityMatrix) = size(PM.matrix) +Base.getindex(PM::ProbabilityMatrix, I::Vararg{Int,N}) where N = getindex(PM.matrix, I...) +Base.setindex!(PM::ProbabilityMatrix, p::Float64, I::Vararg{Int,N}) where N = (PM.matrix[I...] = p) +Base.setindex!(PM::ProbabilityMatrix{N}, X, I::Vararg{Any, N}) where N = (PM.matrix[I...] .= X) + + +function ProbabilityMatrix(diagram::InfluenceDiagram, node::Name) + if node ∉ diagram.Names + throw(DomainError("Node $node should be added as a node to the influence diagram.")) + end + if node ∉ diagram.Names[diagram.C] + throw(DomainError("Only chance nodes can have probability matrices.")) + end + + # Find the node's indices and it's I_c nodes + c = findfirst(x -> x==node, diagram.Names) + nodes = [diagram.I_j[c]..., c] + names = diagram.Names[nodes] + + indices = Vector{Dict{Name, Int}}() + for j in nodes + states = Dict{Name, Int}(state => i + for (i, state) in enumerate(diagram.States[j]) + ) + push!(indices, states) + end + matrix = Array{Float64}(undef, diagram.S[nodes]...) + + return ProbabilityMatrix(names, indices, matrix) +end + +function set_probability!(probability_matrix::ProbabilityMatrix, scenario::Array{String}, probability::Float64) + index = Vector{Int}() + for (i, s) in enumerate(scenario) + if get(probability_matrix.indices[i], s, 0) == 0 + throw(DomainError("Node $(probability_matrix.nodes[i]) does not have a state called $s.")) + else + push!(index, get(probability_matrix.indices[i], s, 0)) + end + end + + probability_matrix[index...] = probability +end + +function set_probability!(probability_matrix::ProbabilityMatrix, scenario::Array{Any}, probabilities::Array{Float64}) + index = Vector{Any}() + for (i, s) in enumerate(scenario) + if isa(s, Colon) + push!(index, s) + elseif get(probability_matrix.indices[i], s, 0) == 0 + throw(DomainError("Node $(probability_matrix.nodes[i]) does not have state $s.")) + else + push!(index, get(probability_matrix.indices[i], s, 0)) + end + end + + probability_matrix[index...] = probabilities +end + + +function add_probabilities!(diagram::InfluenceDiagram, name::Name, probabilities::AbstractArray{Float64, N}) where N c = findfirst(x -> x==name, diagram.Names) + # TODO should there be a check that all cells of array are filled if size(probabilities) == Tuple((diagram.S[n] for n in (diagram.I_j[c]..., c))) - push!(diagram.X, Probabilities(c, probabilities)) + if isa(probabilities, ProbabilityMatrix) + push!(diagram.X, Probabilities(c, probabilities.matrix)) + else + push!(diagram.X, Probabilities(c, probabilities)) + end else throw(DomainError("The dimensions of a probability matrix should match the node's states' and information states' cardinality. Expected $(Tuple((diagram.S[n] for n in (diagram.I_j[c]..., c)))) for node $name, got $(size(probabilities)).")) end @@ -529,7 +602,14 @@ function generate_diagram!(diagram::InfluenceDiagram; positive_path_utility::Bool=false, negative_path_utility::Bool=false) - # Validate influence diagram + # Check correct number of probabilities and consequences were added + if sort([x.c for x in diagram.X]) != diagram.C + throw(DomainError("A probability matrix should be defined for each chance node exactly once.")) + end + if sort([y.v for y in diagram.Y]) != diagram.V + throw(DomainError("A consequence matrix should be defined for each value node exactly once.")) + end + # Sort probabilities and consequences sort!(diagram.X, by = x -> x.c) sort!(diagram.Y, by = x -> x.v) From 736e5d754b29af4ccb8c710199609fae2d142aa1 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 26 Aug 2021 16:00:05 +0300 Subject: [PATCH 051/133] Added UtilityMatrix, set_utilities and edited add_utilities. Also lauched primitive type alias Utility = Float32. --- src/influence_diagram.jl | 95 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 5 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 3741c489..be1125f1 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -258,6 +258,13 @@ end # --- Consequences --- +""" + Utility = Float32 + +Primitive type for utility. Alias for `Float32`. +""" +const Utility = Float32 + """ Consequences{N} <: AbstractArray{Float64, N} @@ -274,7 +281,7 @@ julia> Y(s) """ struct Consequences{N} <: AbstractArray{Float64, N} v::Node - data::Array{Float64, N} + data::Array{Utility, N} end Base.size(Y::Consequences) = size(Y.data) @@ -314,6 +321,9 @@ Default path utility. U = DefaultPathUtility(V, Y) s = (1, 2) U(s) + +t = -100.0 +U(s, t) ``` """ struct DefaultPathUtility <: AbstractPathUtility @@ -478,13 +488,88 @@ function add_probabilities!(diagram::InfluenceDiagram, name::Name, probabilities end end -function AddConsequences!(diagram::InfluenceDiagram, name::Name, consequences::Array{Float64, N}) where N + +# --- Adding Consequences --- + +struct UtilityMatrix{N} <: AbstractArray{Float32, N} + I_v::Vector{Name} + indices::Vector{Dict{Name, Int}} + matrix::Array{Utility, N} +end + +Base.size(UM::UtilityMatrix) = size(UM.matrix) +Base.getindex(UM::UtilityMatrix, I::Vararg{Int,N}) where N = getindex(UM.matrix, I...) +Base.setindex!(UM::UtilityMatrix, y::Float64, I::Vararg{Int,N}) where N = (UM.matrix[I...] = y) +Base.setindex!(UM::UtilityMatrix{N}, Y, I::Vararg{Any, N}) where N = (UM.matrix[I...] .= Y) + + +function UtilityMatrix(diagram::InfluenceDiagram, node::Name) + if node ∉ diagram.Names + throw(DomainError("Node $node should be added as a node to the influence diagram.")) + end + if node ∉ diagram.Names[diagram.V] + throw(DomainError("Only value nodes can have consequence matrices.")) + end + + # Find the node's indexand it's I_v nodes + v = findfirst(x -> x==node, diagram.Names) + I_v = diagram.I_j[v] + names = diagram.Names[I_v] + + indices = Vector{Dict{Name, Int}}() + for j in I_v + states = Dict{Name, Int}(state => i + for (i, state) in enumerate(diagram.States[j]) + ) + push!(indices, states) + end + matrix = Array{Utility}(fill(Inf, diagram.S[I_v]...)) + + return UtilityMatrix(names, indices, matrix) +end + +function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{String}, utility::Float64) + index = Vector{Int}() + for (i, s) in enumerate(scenario) + if get(utility_matrix.indices[i], s, 0) == 0 + throw(DomainError("Node $(utility_matrix.I_v[i]) does not have a state called $s.")) + else + push!(index, get(probability_matrix.indices[i], s, 0)) + end + end + + utility_matrix[index...] = Utility(utility) +end + +function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{Any}, utility::Array{Float64}) + index = Vector{Any}() + for (i, s) in enumerate(scenario) + if isa(s, Colon) + push!(index, s) + elseif get(utility_matrix.indices[i], s, 0) == 0 + throw(DomainError("Node $(utility_matrix.I_v[i]) does not have state $s.")) + else + push!(index, get(utility_matrix.indices[i], s, 0)) + end + end + + # Conversion to Float32 using Utility(), since machine default is Float64 + utility_matrix[index...] = Utility(utility) +end + +function add_utilities!(diagram::InfluenceDiagram, node::Name, utilities::AbstractArray{AbstractFloat, N}) where N v = findfirst(x -> x==name, diagram.Names) + # TODO should there be a check that all cells of array are filled - if size(consequences) == Tuple((diagram.S[n] for n in diagram.I_j[v])) - push!(diagram.Y, Consequences(v, consequences)) + if size(utilities) == Tuple((diagram.S[n] for n in diagram.I_j[v])) + if isa(utilities, UtilityMatrix) + push!(diagram.Y, Consequences(v, utilities.matrix)) + else + # Conversion to Float32 using Utility(), since machine default is Float64 + push!(diagram.Y, Consequences(v, [Utility(u) for u in utilities])) + end else - throw(DomainError("The dimensions of the consequences matrix should match the node's information states' cardinality. Expected $(Tuple((diagram.S[n] for n in diagram.I_j[v]))) for node $name, got $(size(consequences)).")) + throw(DomainError("The dimensions of the utilities matrix should match the node's information states' cardinality. Expected $(Tuple((diagram.S[n] for n in diagram.I_j[v]))) for node $name, got $(size(utilities)).")) end end From 17712caddc13f4ede79ea3976b003a05c9022044 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 26 Aug 2021 16:54:36 +0300 Subject: [PATCH 052/133] Fixed data type in setindex. Data type in set_utility and add_utilities is now Real due to conversions that happen in the function. --- src/influence_diagram.jl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index be1125f1..6486c7f7 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -499,7 +499,7 @@ end Base.size(UM::UtilityMatrix) = size(UM.matrix) Base.getindex(UM::UtilityMatrix, I::Vararg{Int,N}) where N = getindex(UM.matrix, I...) -Base.setindex!(UM::UtilityMatrix, y::Float64, I::Vararg{Int,N}) where N = (UM.matrix[I...] = y) +Base.setindex!(UM::UtilityMatrix, y::Utility, I::Vararg{Int,N}) where N = (UM.matrix[I...] = y) Base.setindex!(UM::UtilityMatrix{N}, Y, I::Vararg{Any, N}) where N = (UM.matrix[I...] .= Y) @@ -528,20 +528,20 @@ function UtilityMatrix(diagram::InfluenceDiagram, node::Name) return UtilityMatrix(names, indices, matrix) end -function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{String}, utility::Float64) +function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{String}, utility::Real) index = Vector{Int}() for (i, s) in enumerate(scenario) if get(utility_matrix.indices[i], s, 0) == 0 throw(DomainError("Node $(utility_matrix.I_v[i]) does not have a state called $s.")) else - push!(index, get(probability_matrix.indices[i], s, 0)) + push!(index, get(utility_matrix.indices[i], s, 0)) end end utility_matrix[index...] = Utility(utility) end -function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{Any}, utility::Array{Float64}) +function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{Any}, utility::Array{T}) where T<:Real index = Vector{Any}() for (i, s) in enumerate(scenario) if isa(s, Colon) @@ -554,14 +554,14 @@ function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{Any}, utili end # Conversion to Float32 using Utility(), since machine default is Float64 - utility_matrix[index...] = Utility(utility) + utility_matrix[index...] = [Utility(u) for u in utility] end -function add_utilities!(diagram::InfluenceDiagram, node::Name, utilities::AbstractArray{AbstractFloat, N}) where N - v = findfirst(x -> x==name, diagram.Names) +function add_utilities!(diagram::InfluenceDiagram, node::Name, utilities::AbstractArray{T, N}) where {N,T<:Real} + v = findfirst(x -> x==node, diagram.Names) # TODO should there be a check that all cells of array are filled - if size(utilities) == Tuple((diagram.S[n] for n in diagram.I_j[v])) + if size(utilities) == Tuple((diagram.S[j] for j in diagram.I_j[v])) if isa(utilities, UtilityMatrix) push!(diagram.Y, Consequences(v, utilities.matrix)) else From d05b4df70a8d2ad4754315da165b7d96742f57e4 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 26 Aug 2021 16:55:11 +0300 Subject: [PATCH 053/133] Code oranising using section headings. --- src/influence_diagram.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 6486c7f7..e3f1d1e4 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -573,6 +573,9 @@ function add_utilities!(diagram::InfluenceDiagram, node::Name, utilities::Abstra end end + +# --- Generating Arcs --- + function validate_structure(Nodes::Vector{AbstractNode}, C_and_D::Vector{AbstractNode}, n_CD::Int, V::Vector{AbstractNode}, n_V::Int) # Validating node structure if n_CD == 0 @@ -681,6 +684,9 @@ function generate_arcs!(diagram::InfluenceDiagram) end + +# --- Generating Diagram --- + function generate_diagram!(diagram::InfluenceDiagram; default_probability::Bool=true, default_utility::Bool=true, From b5208bf2424cc7fbf648e4878d55b800e01ae4cc Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 26 Aug 2021 16:55:43 +0300 Subject: [PATCH 054/133] Added UtilityMatrix functionalities to DecisionProgramming.jl --- src/DecisionProgramming.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/DecisionProgramming.jl b/src/DecisionProgramming.jl index 2366ebac..39b31e22 100644 --- a/src/DecisionProgramming.jl +++ b/src/DecisionProgramming.jl @@ -32,7 +32,9 @@ export Node, ProbabilityMatrix, set_probability!, add_probabilities!, - add_consequences! + UtilityMatrix, + set_utility!, + add_utilities! export DecisionVariables, PathCompatibilityVariables, From 2e63449a968aba9c546e9bf0f13463e51e19fda6 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 26 Aug 2021 17:46:43 +0300 Subject: [PATCH 055/133] Implemented the check that all elements of a utility matrix have a value. Also Fixed some issues with the data type conversion using Utility() --- src/DecisionProgramming.jl | 3 ++- src/influence_diagram.jl | 50 +++++++++++++++++++++----------------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/DecisionProgramming.jl b/src/DecisionProgramming.jl index 39b31e22..34d60a37 100644 --- a/src/DecisionProgramming.jl +++ b/src/DecisionProgramming.jl @@ -17,7 +17,8 @@ export Node, Path, paths, Probabilities, - Consequences, + Utility, + Utilities, AbstractPathProbability, DefaultPathProbability, AbstractPathUtility, diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index e3f1d1e4..ba8783d1 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -256,7 +256,7 @@ function (P::DefaultPathProbability)(s::Path) end -# --- Consequences --- +# --- Utilities --- """ Utility = Float32 @@ -266,30 +266,36 @@ Primitive type for utility. Alias for `Float32`. const Utility = Float32 """ - Consequences{N} <: AbstractArray{Float64, N} + Utilities{N} <: AbstractArray{Utility, N} State utilities. # Examples ```julia-repl julia> vals = [1.0 -2.0; 3.0 4.0] -julia> Y = Consequences(3, vals) +julia> Y = Utilities(3, vals) julia> s = (1, 2) julia> Y(s) -2.0 ``` """ -struct Consequences{N} <: AbstractArray{Float64, N} +struct Utilities{N} <: AbstractArray{Utility, N} v::Node data::Array{Utility, N} + function Utilities(v::Node, data::Array{Utility, N}) where N + if any(isinf(u) for u in data) + throw(DomainError("A value should be defined for each element of a utility matrix.")) + end + new{N}(v, data) + end end -Base.size(Y::Consequences) = size(Y.data) -Base.IndexStyle(::Type{<:Consequences}) = IndexLinear() -Base.getindex(Y::Consequences, i::Int) = getindex(Y.data, i) -Base.getindex(Y::Consequences, I::Vararg{Int,N}) where N = getindex(Y.data, I...) +Base.size(Y::Utilities) = size(Y.data) +Base.IndexStyle(::Type{<:Utilities}) = IndexLinear() +Base.getindex(Y::Utilities, i::Int) = getindex(Y.data, i) +Base.getindex(Y::Utilities, I::Vararg{Int,N}) where N = getindex(Y.data, I...) -(Y::Consequences)(s::Path) = Y[s...] +(Y::Utilities)(s::Path) = Y[s...] # --- Path Utility --- @@ -328,14 +334,14 @@ U(s, t) """ struct DefaultPathUtility <: AbstractPathUtility I_v::Vector{Vector{Node}} - Y::Vector{Consequences} + Y::Vector{Utilities} end function (U::DefaultPathUtility)(s::Path) sum(Y(s[I_v]) for (I_v, Y) in zip(U.I_v, U.Y)) end -function (U::DefaultPathUtility)(s::Path, t::Float64) +function (U::DefaultPathUtility)(s::Path, t::Utility) U(s) + t end @@ -352,10 +358,10 @@ mutable struct InfluenceDiagram D::Vector{Node} V::Vector{Node} X::Vector{Probabilities} - Y::Vector{Consequences} + Y::Vector{Utilities} P::AbstractPathProbability U::AbstractPathUtility - translation::Float64 + translation::Utility function InfluenceDiagram() new(Vector{AbstractNode}()) end @@ -489,9 +495,9 @@ function add_probabilities!(diagram::InfluenceDiagram, name::Name, probabilities end -# --- Adding Consequences --- +# --- Adding Utilities --- -struct UtilityMatrix{N} <: AbstractArray{Float32, N} +struct UtilityMatrix{N} <: AbstractArray{Utility, N} I_v::Vector{Name} indices::Vector{Dict{Name, Int}} matrix::Array{Utility, N} @@ -559,14 +565,13 @@ end function add_utilities!(diagram::InfluenceDiagram, node::Name, utilities::AbstractArray{T, N}) where {N,T<:Real} v = findfirst(x -> x==node, diagram.Names) - # TODO should there be a check that all cells of array are filled if size(utilities) == Tuple((diagram.S[j] for j in diagram.I_j[v])) if isa(utilities, UtilityMatrix) - push!(diagram.Y, Consequences(v, utilities.matrix)) + push!(diagram.Y, Utilities(Node(v), utilities.matrix)) else # Conversion to Float32 using Utility(), since machine default is Float64 - push!(diagram.Y, Consequences(v, [Utility(u) for u in utilities])) + push!(diagram.Y, Utilities(Node(v), [Utility(u) for u in utilities])) end else throw(DomainError("The dimensions of the utilities matrix should match the node's information states' cardinality. Expected $(Tuple((diagram.S[n] for n in diagram.I_j[v]))) for node $name, got $(size(utilities)).")) @@ -680,7 +685,7 @@ function generate_arcs!(diagram::InfluenceDiagram) diagram.V = V # Declaring X and Y diagram.X = Vector{Probabilities}() - diagram.Y = Vector{Consequences}() + diagram.Y = Vector{Utilities}() end @@ -712,11 +717,12 @@ function generate_diagram!(diagram::InfluenceDiagram; if default_utility diagram.U = DefaultPathUtility(diagram.I_j[diagram.V], diagram.Y) if positive_path_utility - diagram.translation = 1 - minimum(diagram.U(s) for s in paths(diagram.S)) + # Conversion to Float32 using Utility(), since machine default is Float64 + diagram.translation = Utility(1 - minimum(diagram.U(s) for s in paths(diagram.S))) elseif negative_path_utility - diagram.translation = -1 - maximum(diagram.U(s) for s in paths(diagram.S)) + diagram.translation = Utility(-1 - maximum(diagram.U(s) for s in paths(diagram.S))) else - diagram.translation = 0 + diagram.translation = Utility(0) end end From c757a8a9435b1fd582cc266423e85b9d9d29e826 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 27 Aug 2021 06:40:10 +0300 Subject: [PATCH 056/133] Fixed probability matrix to be declared as matrix of zeros. --- src/influence_diagram.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index ba8783d1..cef2bb0f 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -445,7 +445,7 @@ function ProbabilityMatrix(diagram::InfluenceDiagram, node::Name) ) push!(indices, states) end - matrix = Array{Float64}(undef, diagram.S[nodes]...) + matrix = fill(0.0, diagram.S[nodes]...) return ProbabilityMatrix(names, indices, matrix) end @@ -479,12 +479,12 @@ function set_probability!(probability_matrix::ProbabilityMatrix, scenario::Array end -function add_probabilities!(diagram::InfluenceDiagram, name::Name, probabilities::AbstractArray{Float64, N}) where N - c = findfirst(x -> x==name, diagram.Names) - # TODO should there be a check that all cells of array are filled +function add_probabilities!(diagram::InfluenceDiagram, node::Name, probabilities::AbstractArray{Float64, N}) where N + c = findfirst(x -> x==node, diagram.Names) - if size(probabilities) == Tuple((diagram.S[n] for n in (diagram.I_j[c]..., c))) + if size(probabilities) == Tuple((diagram.S[j] for j in (diagram.I_j[c]..., c))) if isa(probabilities, ProbabilityMatrix) + # Check that probabilities sum to one happesn in Probabilities push!(diagram.X, Probabilities(c, probabilities.matrix)) else push!(diagram.X, Probabilities(c, probabilities)) From c97348a52b6af6065659ddd0c2fde820e5d90cd6 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 27 Aug 2021 06:57:04 +0300 Subject: [PATCH 057/133] Moved validation that only one probability/utility matrix is added per node to a more logical place. --- src/influence_diagram.jl | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index cef2bb0f..73172a88 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -482,6 +482,10 @@ end function add_probabilities!(diagram::InfluenceDiagram, node::Name, probabilities::AbstractArray{Float64, N}) where N c = findfirst(x -> x==node, diagram.Names) + if c ∈ [j.c for j in diagram.X] + throw(DomainError("Probabilities should be added only once for each node.")) + end + if size(probabilities) == Tuple((diagram.S[j] for j in (diagram.I_j[c]..., c))) if isa(probabilities, ProbabilityMatrix) # Check that probabilities sum to one happesn in Probabilities @@ -565,6 +569,10 @@ end function add_utilities!(diagram::InfluenceDiagram, node::Name, utilities::AbstractArray{T, N}) where {N,T<:Real} v = findfirst(x -> x==node, diagram.Names) + + if v ∈ [j.v for j in diagram.Y] + throw(DomainError("Utilities should be added only once for each node.")) + end if size(utilities) == Tuple((diagram.S[j] for j in diagram.I_j[v])) if isa(utilities, UtilityMatrix) @@ -698,13 +706,7 @@ function generate_diagram!(diagram::InfluenceDiagram; positive_path_utility::Bool=false, negative_path_utility::Bool=false) - # Check correct number of probabilities and consequences were added - if sort([x.c for x in diagram.X]) != diagram.C - throw(DomainError("A probability matrix should be defined for each chance node exactly once.")) - end - if sort([y.v for y in diagram.Y]) != diagram.V - throw(DomainError("A consequence matrix should be defined for each value node exactly once.")) - end + # Sort probabilities and consequences sort!(diagram.X, by = x -> x.c) sort!(diagram.Y, by = x -> x.v) From c239a442c07d27ad73b55054cd3f7d9b00ff174d Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 27 Aug 2021 06:59:02 +0300 Subject: [PATCH 058/133] Changed used car buyer to use new function names. --- examples/used_car_buyer.jl | 40 ++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/examples/used_car_buyer.jl b/examples/used_car_buyer.jl index 804506f1..350c73f8 100644 --- a/examples/used_car_buyer.jl +++ b/examples/used_car_buyer.jl @@ -6,32 +6,42 @@ using DecisionProgramming @info("Creating the influence diagram.") diagram = InfluenceDiagram() -AddNode!(diagram, ChanceNode("O", [], ["lemon", "peach"])) -AddNode!(diagram, ChanceNode("R", ["O", "T"], ["no test", "lemon", "peach"])) +add_node!(diagram, ChanceNode("O", [], ["lemon", "peach"])) +add_node!(diagram, ChanceNode("R", ["O", "T"], ["no test", "lemon", "peach"])) -AddNode!(diagram, DecisionNode("T", [], ["no test", "test"])) -AddNode!(diagram, DecisionNode("A", ["R"], ["buy without guarantee", "buy with guarantee", "don't buy"])) +add_node!(diagram, DecisionNode("T", [], ["no test", "test"])) +add_node!(diagram, DecisionNode("A", ["R"], ["buy without guarantee", "buy with guarantee", "don't buy"])) -AddNode!(diagram, ValueNode("V1", ["T"])) -AddNode!(diagram, ValueNode("V2", ["A"])) -AddNode!(diagram, ValueNode("V3", ["O", "A"])) +add_node!(diagram, ValueNode("V1", ["T"])) +add_node!(diagram, ValueNode("V2", ["A"])) +add_node!(diagram, ValueNode("V3", ["O", "A"])) -GenerateArcs!(diagram) +generate_arcs!(diagram) +X_O = ProbabilityMatrix(diagram, "O") +set_probability!(X_O, ["peach"], 0.8) +set_probability!(X_O, ["lemon"], 0.2) +add_probabilities!(diagram, "O", X_O) -X_R = zeros(2, 2, 3) + +X_R = ProbabilityMatrix(diagram, "R") X_R[1, 1, :] = [1,0,0] X_R[1, 2, :] = [0,1,0] X_R[2, 1, :] = [1,0,0] X_R[2, 2, :] = [0,0,1] -AddProbabilities!(diagram, "R", X_R) -AddProbabilities!(diagram, "O", [0.2, 0.8]) +add_probabilities!(diagram, "R", X_R) + +add_utilities!(diagram, "V1", [0, -25]) +add_utilities!(diagram, "V2", [100, 40, 0]) -AddConsequences!(diagram, "V1", [0.0, -25.0]) -AddConsequences!(diagram, "V2", [100.0, 40.0, 0.0]) -AddConsequences!(diagram, "V3", [-200.0 0.0 0.0; -40.0 -20.0 0.0]) +Y_V3 = UtilityMatrix(diagram, "V3") +set_utility!(Y_V3, ["lemon", "buy without guarantee"], -200) +set_utility!(Y_V3, ["lemon", "buy with guarantee"], 0) +set_utility!(Y_V3, ["lemon", "don't buy"], 0) +set_utility!(Y_V3, ["peach", :], [-40, -20, 0]) +add_utilities!(diagram, "V3", Y_V3) -GenerateDiagram!(diagram) +generate_diagram!(diagram) @info("Creating the decision model.") From fc5c98ce638a179048872d0d30f02992ae9a00df Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 27 Aug 2021 07:00:47 +0300 Subject: [PATCH 059/133] Changed pig breeding to use new function names. --- examples/pig_breeding.jl | 68 +++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/examples/pig_breeding.jl b/examples/pig_breeding.jl index d772e5fd..83a78403 100644 --- a/examples/pig_breeding.jl +++ b/examples/pig_breeding.jl @@ -7,51 +7,47 @@ const N = 4 @info("Creating the influence diagram.") diagram = InfluenceDiagram() -AddNode!(diagram, ChanceNode("H1", [], ["ill", "healthy"])) +add_node!(diagram, ChanceNode("H1", [], ["ill", "healthy"])) for i in 1:N-1 # Testing result - AddNode!(diagram, ChanceNode("T$i", ["H$i"], ["positive", "negative"])) + add_node!(diagram, ChanceNode("T$i", ["H$i"], ["positive", "negative"])) # Decision to treat - AddNode!(diagram, DecisionNode("D$i", ["T$i"], ["treat", "pass"])) + add_node!(diagram, DecisionNode("D$i", ["T$i"], ["treat", "pass"])) # Cost of treatment - AddNode!(diagram, ValueNode("C$i", ["D$i"])) + add_node!(diagram, ValueNode("C$i", ["D$i"])) # Health of next period - AddNode!(diagram, ChanceNode("H$(i+1)", ["H$(i)", "D$(i)"], ["ill", "healthy"])) + add_node!(diagram, ChanceNode("H$(i+1)", ["H$(i)", "D$(i)"], ["ill", "healthy"])) end -AddNode!(diagram, ValueNode("SP", ["H$N"])) - -GenerateArcs!(diagram) -# Declare proability matrix for health nodes -X_H = zeros(2, 2, 2) -X_H[2, 2, 1] = 0.2 -X_H[2, 2, 2] = 1.0 - X_H[2, 2, 1] -X_H[2, 1, 1] = 0.1 -X_H[2, 1, 2] = 1.0 - X_H[2, 1, 1] -X_H[1, 2, 1] = 0.9 -X_H[1, 2, 2] = 1.0 - X_H[1, 2, 1] -X_H[1, 1, 1] = 0.5 -X_H[1, 1, 2] = 1.0 - X_H[1, 1, 1] - -# Declare proability matrix for test results nodes -X_T = zeros(2, 2) -X_T[1, 1] = 0.8 -X_T[1, 2] = 1.0 - X_T[1, 1] -X_T[2, 2] = 0.9 -X_T[2, 1] = 1.0 - X_T[2, 2] - -AddProbabilities!(diagram, "H1", [0.1, 0.9]) +add_node!(diagram, ValueNode("SP", ["H$N"])) + +generate_arcs!(diagram) + +# Add probabilities for node H1 +add_probabilities!(diagram, "H1", [0.1, 0.9]) + +# Declare proability matrix for health nodes H_2, ... H_N-1, which have identical information sets and states +X_H = ProbabilityMatrix(diagram, "H2") +set_probability!(X_H, ["healthy", "pass", :], [0.2, 0.8]) +set_probability!(X_H, ["healthy", "treat", :], [0.1, 0.9]) +set_probability!(X_H, ["ill", "pass", :], [0.9, 0.1]) +set_probability!(X_H, ["ill", "treat", :], [0.5, 0.5]) + +# Declare proability matrix for test result nodes T_1...T_N +X_T = ProbabilityMatrix(diagram, "T1") +set_probability!(X_T, ["ill", "positive"], 0.8) +set_probability!(X_T, ["ill", "negative"], 0.2) +set_probability!(X_T, ["healthy", "negative"], 0.9) +set_probability!(X_T, ["healthy", "positive"], 0.1) + for i in 1:N-1 - # Testing result - AddProbabilities!(diagram, "T$i", X_T) - # Cost of treatment - AddConsequences!(diagram, "C$i", [-100.0, 0.0]) - # Health of next period - AddProbabilities!(diagram, "H$(i+1)", X_H) + add_probabilities!(diagram, "T$i", X_T) + add_utilities!(diagram, "C$i", [-100.0, 0.0]) + add_probabilities!(diagram, "H$(i+1)", X_H) end -# Selling price -AddConsequences!(diagram, "SP", [300.0, 1000.0]) -GenerateDiagram!(diagram, positive_path_utility = true) +add_utilities!(diagram, "SP", [300.0, 1000.0]) + +generate_diagram!(diagram, positive_path_utility = true) @info("Creating the decision model.") From 5fce65491c0d01b484afa22670691dcc39d574cc Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 27 Aug 2021 07:09:22 +0300 Subject: [PATCH 060/133] Changed n_monitoring to use new function names. --- examples/n_monitoring.jl | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/examples/n_monitoring.jl b/examples/n_monitoring.jl index 20041bcc..1ddce2ff 100644 --- a/examples/n_monitoring.jl +++ b/examples/n_monitoring.jl @@ -12,22 +12,22 @@ fortification(k, a) = [c_k[k], 0][a] @info("Creating the influence diagram.") diagram = InfluenceDiagram() -AddNode!(diagram, ChanceNode("L", [], ["high", "low"])) +add_node!(diagram, ChanceNode("L", [], ["high", "low"])) for i in 1:N - AddNode!(diagram, ChanceNode("R$i", ["L"], ["high", "low"])) - AddNode!(diagram, DecisionNode("A$i", ["R$i"], ["yes", "no"])) + add_node!(diagram, ChanceNode("R$i", ["L"], ["high", "low"])) + add_node!(diagram, DecisionNode("A$i", ["R$i"], ["yes", "no"])) end -AddNode!(diagram, ChanceNode("F", ["L", ["A$i" for i in 1:N]...], ["failure", "success"])) +add_node!(diagram, ChanceNode("F", ["L", ["A$i" for i in 1:N]...], ["failure", "success"])) -AddNode!(diagram, ValueNode("T", ["F", ["A$i" for i in 1:N]...])) +add_node!(diagram, ValueNode("T", ["F", ["A$i" for i in 1:N]...])) -GenerateArcs!(diagram) +generate_arcs!(diagram) X_L = [rand(), 0] X_L[2] = 1.0 - X_L[1] -AddProbabilities!(diagram, "L", X_L) +add_probabilities!(diagram, "L", X_L) for i in 1:N x, y = rand(2) @@ -36,7 +36,7 @@ for i in 1:N X_R[1, 2] = 1.0 - X_R[1, 1] X_R[2, 2] = max(y, 1-y) X_R[2, 1] = 1.0 - X_R[2, 2] - AddProbabilities!(diagram, "R$i", X_R) + add_probabilities!(diagram, "R$i", X_R) end for i in [1] @@ -49,23 +49,23 @@ for i in [1] X_F[2, s..., 1] = min(y, 1-y) / d X_F[2, s..., 2] = 1.0 - X_F[2, s..., 1] end - AddProbabilities!(diagram, "F", X_F) + add_probabilities!(diagram, "F", X_F) end -Y_T = zeros([2 for i in 1:N]..., 2) +Y_T = UtilityMatrix(diagram, "T") for s in paths([2 for i in 1:N]) cost = sum(-fortification(k, a) for (k, a) in enumerate(s)) Y_T[1, s...] = cost + 0 Y_T[2, s...] = cost + 100 end -AddConsequences!(diagram, "T", Y_T) +add_utilities!(diagram, "T", Y_T) -GenerateDiagram!(diagram)#, positive_path_utility=true) +generate_diagram!(diagram, positive_path_utility=true) model = Model() z = DecisionVariables(model, diagram) -x_s = PathCompatibilityVariables(model, diagram, z)#, probability_cut = false) +x_s = PathCompatibilityVariables(model, diagram, z, probability_cut = false) EV = expected_value(model, diagram, x_s) @objective(model, Max, EV) From bb8272ebdf5eba3b86868bcc496297959c449636 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 27 Aug 2021 07:10:28 +0300 Subject: [PATCH 061/133] Chaged utility matrix setindex! function to accetp y::T<:Real because the conversion happens automatically since utilitymatrix.matrix is of type Utility. --- src/influence_diagram.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 73172a88..7afcff44 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -509,7 +509,7 @@ end Base.size(UM::UtilityMatrix) = size(UM.matrix) Base.getindex(UM::UtilityMatrix, I::Vararg{Int,N}) where N = getindex(UM.matrix, I...) -Base.setindex!(UM::UtilityMatrix, y::Utility, I::Vararg{Int,N}) where N = (UM.matrix[I...] = y) +Base.setindex!(UM::UtilityMatrix, y::T, I::Vararg{Int,N}) where {N, T<:Real} = (UM.matrix[I...] = y) Base.setindex!(UM::UtilityMatrix{N}, Y, I::Vararg{Any, N}) where N = (UM.matrix[I...] .= Y) @@ -569,7 +569,7 @@ end function add_utilities!(diagram::InfluenceDiagram, node::Name, utilities::AbstractArray{T, N}) where {N,T<:Real} v = findfirst(x -> x==node, diagram.Names) - + if v ∈ [j.v for j in diagram.Y] throw(DomainError("Utilities should be added only once for each node.")) end From 7798bf40e263214d61523b3b90e5166ca23d7c77 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 27 Aug 2021 07:36:02 +0300 Subject: [PATCH 062/133] Change StateProbabilities to allow fixed nodes and states to be given using their Name. --- src/analysis.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/analysis.jl b/src/analysis.jl index 7998972c..63eb2a49 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -155,10 +155,13 @@ state = 2 StateProbabilities(S, P, Z, node, state, prev) ``` """ -function StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy, node::Node, state::State, prior_probabilities::StateProbabilities) - prior = prior_probabilities.probs[node][state] +function StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy, node::Name, state::Name, prior_probabilities::StateProbabilities) + node_index = findfirst(j -> j ==node, diagram.Names) + state_index = findfirst(j -> j == state, diagram.States[node_index]) + + prior = prior_probabilities.probs[node_index][state_index] fixed = prior_probabilities.fixed - push!(fixed, node => state) + push!(fixed, node_index => state_index) probs = Dict(i => zeros(diagram.S[i]) for i in 1:length(diagram.S)) for s in CompatiblePaths(diagram, Z, fixed), i in 1:length(diagram.S) probs[i][s[i]] += diagram.P(s) / prior #TODO double check that this is correct From 27185b7428341be9e59a20e7d5f759f0b512ac1c Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 27 Aug 2021 07:41:25 +0300 Subject: [PATCH 063/133] Updated print_state_probabilities to use node names instead of indices in the parameter nodes. --- src/printing.jl | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/printing.jl b/src/printing.jl index 9d17d641..9a22dfb4 100644 --- a/src/printing.jl +++ b/src/printing.jl @@ -65,22 +65,24 @@ print_state_probabilities(sprobs, [c.j for c in C]) print_state_probabilities(sprobs, [d.j for d in D]) ``` """ -function print_state_probabilities(sprobs::StateProbabilities, nodes::Vector{Node}; prob_fmt="%f") - probs = sprobs.probs - fixed = sprobs.fixed +function print_state_probabilities(diagram::InfluenceDiagram, state_probabilities::StateProbabilities, nodes::Vector{Name}; prob_fmt="%f") + node_indices = [findfirst(j -> j ==node, diagram.Names) for node in nodes] + + probs = state_probabilities.probs + fixed = state_probabilities.fixed prob(p, state) = if 1≤state≤length(p) p[state] else NaN end fix_state(i) = if i∈keys(fixed) string(fixed[i]) else "" end # Maximum number of states - limit = maximum(length(probs[i]) for i in nodes) + limit = maximum(length(probs[i]) for i in node_indices) states = 1:limit df = DataFrame() df[!, :Node] = nodes for state in states - df[!, Symbol("State $state")] = [prob(probs[i], state) for i in nodes] + df[!, Symbol("State $state")] = [prob(probs[i], state) for i in node_indices] end - df[!, Symbol("Fixed state")] = [fix_state(i) for i in nodes] + df[!, Symbol("Fixed state")] = [fix_state(i) for i in node_indices] pretty_table(df; formatters = ft_printf(prob_fmt, (first(states)+1):(last(states)+1))) end From e1ec518fea893d825308d95f153132beeb3b0e26 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 27 Aug 2021 10:59:44 +0300 Subject: [PATCH 064/133] Deleted unnecessary Utility() conversions. --- src/influence_diagram.jl | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 7afcff44..65d5947d 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -548,7 +548,7 @@ function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{String}, ut end end - utility_matrix[index...] = Utility(utility) + utility_matrix[index...] = utility end function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{Any}, utility::Array{T}) where T<:Real @@ -563,8 +563,7 @@ function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{Any}, utili end end - # Conversion to Float32 using Utility(), since machine default is Float64 - utility_matrix[index...] = [Utility(u) for u in utility] + utility_matrix[index...] = utility end function add_utilities!(diagram::InfluenceDiagram, node::Name, utilities::AbstractArray{T, N}) where {N,T<:Real} @@ -720,11 +719,11 @@ function generate_diagram!(diagram::InfluenceDiagram; diagram.U = DefaultPathUtility(diagram.I_j[diagram.V], diagram.Y) if positive_path_utility # Conversion to Float32 using Utility(), since machine default is Float64 - diagram.translation = Utility(1 - minimum(diagram.U(s) for s in paths(diagram.S))) + diagram.translation = 1 - minimum(diagram.U(s) for s in paths(diagram.S)) elseif negative_path_utility - diagram.translation = Utility(-1 - maximum(diagram.U(s) for s in paths(diagram.S))) + diagram.translation = -1 - maximum(diagram.U(s) for s in paths(diagram.S)) else - diagram.translation = Utility(0) + diagram.translation = 0 end end From 89d211055d5e3837703e885af8b21994ec8e01b9 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 27 Aug 2021 11:03:09 +0300 Subject: [PATCH 065/133] Specified ambigous Int types. --- src/analysis.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/analysis.jl b/src/analysis.jl index 63eb2a49..7d2e05f2 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -34,7 +34,7 @@ function CompatiblePaths(diagram::InfluenceDiagram, Z::DecisionStrategy) end function compatible_path(S::States, C::Vector{Node}, Z::DecisionStrategy, s_C::Path) - s = Array{Int}(undef, length(S)) + s = Array{State}(undef, length(S)) for (c, s_C_j) in zip(C, s_C) s[c] = s_C_j end @@ -49,7 +49,7 @@ function Base.iterate(S_Z::CompatiblePaths) iter = paths(S_Z.S[S_Z.C]) else ks = sort(collect(keys(S_Z.fixed))) - fixed = Dict{Int, Int}(i => S_Z.fixed[k] for (i, k) in enumerate(ks)) + fixed = Dict{Node, State}(i => S_Z.fixed[k] for (i, k) in enumerate(ks)) iter = paths(S_Z.S[S_Z.C], fixed) end next = iterate(iter) From f96f30fdf495299fe888b14a88e47dc86c60d3f3 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 27 Aug 2021 11:44:19 +0300 Subject: [PATCH 066/133] Improved readability in generate arcs function. --- src/influence_diagram.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 65d5947d..e68b8b3e 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -628,8 +628,8 @@ function generate_arcs!(diagram::InfluenceDiagram) # Declare vectors for results (final resting place InfluenceDiagram.Names and InfluenceDiagram.I_j) Names = Vector{Name}(undef, n_CD+n_V) I_j = Vector{Vector{Node}}(undef, n_CD+n_V) - State_names = Vector{Vector{Name}}() - states = Vector{State}() + states = Vector{Vector{Name}}() + S = Vector{State}(undef, n_CD) C = Vector{Node}() D = Vector{Node}() V = Vector{Node}() @@ -649,10 +649,10 @@ function generate_arcs!(diagram::InfluenceDiagram) push!(indices, j.name => index) push!(indexed_nodes, j.name) # Update results - Names[index] = Name(j.name) #TODO datatype conversion happens here, should we use push! ? + Names[index] = Name(j.name) #TODO datatype conversion happens here, should we use push! ? I_j[index] = map(x -> Node(indices[x]), j.I_j) - push!(State_names, j.states) - push!(states, State(length(j.states))) + push!(states, j.states) + S[index] = State(length(j.states)) if isa(j, ChanceNode) push!(C, Node(index)) else @@ -685,8 +685,8 @@ function generate_arcs!(diagram::InfluenceDiagram) diagram.Names = Names diagram.I_j = I_j - diagram.States = State_names - diagram.S = States(states) + diagram.States = states + diagram.S = States(S) diagram.C = C diagram.D = D diagram.V = V From f1b0ef5dceedbd7e4736199a92f9b7503561ca2f Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 27 Aug 2021 11:46:30 +0300 Subject: [PATCH 067/133] Deleted outer construction function State since it's not needed with the new interface. --- src/influence_diagram.jl | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index e68b8b3e..8abf1d6e 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -88,24 +88,6 @@ Base.getindex(S::States, i::Int) = getindex(S.vals, i) Base.length(S::States) = length(S.vals) Base.eltype(S::States) = eltype(S.vals) -""" - function States(states::Vector{Tuple{State, Vector{Node}}}) - -Construct states from vector of (state, nodes) tuples. - -# Examples -```julia-repl -julia> S = States([(2, [1, 3]), (3, [2, 4, 5])]) -States([2, 3, 2, 3, 3]) -``` -""" -function States(states::Vector{Tuple{State, Vector{Node}}}) # TODO should this just be gotten rid of? - S_j = Vector{State}(undef, sum(length(j) for (_, j) in states)) - for (s, j) in states - S_j[j] .= s - end - States(S_j) -end # --- Paths --- From b128fc32641fdbd18347721b99f72976375a53da Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 27 Aug 2021 11:47:08 +0300 Subject: [PATCH 068/133] Added some explicit conversions to allow for changing Node primitive type. --- src/influence_diagram.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 8abf1d6e..b6c9ed18 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -471,9 +471,9 @@ function add_probabilities!(diagram::InfluenceDiagram, node::Name, probabilities if size(probabilities) == Tuple((diagram.S[j] for j in (diagram.I_j[c]..., c))) if isa(probabilities, ProbabilityMatrix) # Check that probabilities sum to one happesn in Probabilities - push!(diagram.X, Probabilities(c, probabilities.matrix)) + push!(diagram.X, Probabilities(Node(c), probabilities.matrix)) else - push!(diagram.X, Probabilities(c, probabilities)) + push!(diagram.X, Probabilities(Node(c), probabilities)) end else throw(DomainError("The dimensions of a probability matrix should match the node's states' and information states' cardinality. Expected $(Tuple((diagram.S[n] for n in (diagram.I_j[c]..., c)))) for node $name, got $(size(probabilities)).")) From ddfb8362f3e2aa76e7be1e283bd26cb6775cae42 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 27 Aug 2021 11:47:47 +0300 Subject: [PATCH 069/133] Changed node primitive type to Int16. --- src/influence_diagram.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index b6c9ed18..976a92da 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -4,11 +4,11 @@ using Base.Iterators: product # --- Nodes and States --- """ - Node = Int + Node = Int16 Primitive type for node index. Alias for `Int`. """ -const Node = Int +const Node = Int16 """ Name = String From 28cb5c48f6704bb84d819499acdb2b43c914b7b5 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 27 Aug 2021 16:06:56 +0300 Subject: [PATCH 070/133] Created FixedPath struct and outer construction functions for FixedPath and ForbiddenPaths. --- src/analysis.jl | 14 +++--- src/decision_model.jl | 6 +-- src/influence_diagram.jl | 94 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 104 insertions(+), 10 deletions(-) diff --git a/src/analysis.jl b/src/analysis.jl index 7d2e05f2..4d7b208e 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -3,7 +3,7 @@ struct CompatiblePaths S::States C::Vector{Node} Z::DecisionStrategy - fixed::Dict{Node, State} + fixed::FixedPath function CompatiblePaths(diagram, Z, fixed) if !all(k∈Set(diagram.C) for k in keys(fixed)) throw(DomainError("You can only fix chance states.")) @@ -13,7 +13,7 @@ struct CompatiblePaths end """ - CompatiblePaths(S::States, C::Vector{ChanceNode}, Z::DecisionStrategy) + CompatiblePaths(diagram::InfluenceDiagram, Z::DecisionStrategy) Interface for iterating over paths that are compatible and active given influence diagram and decision strategy. @@ -23,12 +23,11 @@ Interface for iterating over paths that are compatible and active given influenc # Examples ```julia -for s in CompatiblePaths(S, C, Z) +for s in CompatiblePaths(diagram, Z) ... end ``` """ - function CompatiblePaths(diagram::InfluenceDiagram, Z::DecisionStrategy) CompatiblePaths(diagram, Z, Dict{Node, State}()) end @@ -130,13 +129,16 @@ function UtilityDistribution(diagram::InfluenceDiagram, Z::DecisionStrategy) end """ - StateProbabilities + struct StateProbabilities + probs::Dict{Node, Vector{Float64}} + fixed::FixedPath + end StateProbabilities type. """ struct StateProbabilities probs::Dict{Node, Vector{Float64}} - fixed::Dict{Node, State} + fixed::FixedPath end """ diff --git a/src/decision_model.jl b/src/decision_model.jl index bd9f1a36..7c219fbe 100644 --- a/src/decision_model.jl +++ b/src/decision_model.jl @@ -98,7 +98,7 @@ end names::Bool=false, name::String="x", forbidden_paths::Vector{ForbiddenPath}=ForbiddenPath[], - fixed::Dict{Node, State}=Dict{Node, State}(), + fixed::FixedPath=Dict{Node, State}(), probability_cut::Bool=true) Create path compatibility variables and constraints. @@ -114,7 +114,7 @@ Create path compatibility variables and constraints. - `forbidden_paths::Vector{ForbiddenPath}`: The forbidden subpath structures. Path compatibility variables will not be generated for paths that include forbidden subpaths. -- `fixed::Dict{Node, State}`: Path compatibility variable will not be generated +- `fixed::FixedPath`: Path compatibility variable will not be generated for paths which do not include these fixed subpaths. - `probability_cut` Includes probability cut constraint in the optimisation model. @@ -129,7 +129,7 @@ function PathCompatibilityVariables(model::Model, names::Bool=false, name::String="x", forbidden_paths::Vector{ForbiddenPath}=ForbiddenPath[], - fixed::Dict{Node, State}=Dict{Node, State}(), + fixed::FixedPath=Dict{Node, State}(), probability_cut::Bool=true) if !isempty(forbidden_paths) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 976a92da..c391f9b6 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -142,7 +142,8 @@ ForbiddenPath type. # Examples ```julia -ForbiddenPath[ +julia> ForbiddenPath(([1, 2], Set([(1, 2)]))) +julia> ForbiddenPath[ ([1, 2], Set([(1, 2)])), ([3, 4, 5], Set([(1, 2, 3), (3, 4, 5)])) ] @@ -151,6 +152,20 @@ ForbiddenPath[ const ForbiddenPath = Tuple{Vector{Node}, Set{Path}} +""" + const FixedPath = Dict{Node, State} + +FixedPath type. + +# Examples +```julia +julia> FixedPath(Dict(1=>1, 2=>3)) +] +``` +""" +const FixedPath = Dict{Node, State} + + # --- Probabilities --- """ @@ -711,6 +726,83 @@ function generate_diagram!(diagram::InfluenceDiagram; end +# --- ForbiddenPath and FixedPath outer construction functions --- +""" + function ForbiddenPath(diagram::InfluenceDiagram, nodes::Vector{Name}, paths::Vector{NTuple{N, Name}}) where N + +ForbiddenPath outer construction function. Create ForbiddenPath variable. + +# Arguments +- `diagram::InfluenceDiagram`: Influence diagram structure +- `nodes::Vector{Name}`: Vector of nodes involved in forbidden paths. Identified by their names. +- `paths`::Vector{NTuple{N, Name}}`: Vector of tuples defining the forbidden combinations of states. States identified by their names. + +# Example +```julia +julia> ForbiddenPath(diagram, ["R1", "R2"], [("high", "low"), ("low", "high")]) +``` +""" +function ForbiddenPath(diagram::InfluenceDiagram, nodes::Vector{Name}, paths::Vector{NTuple{N, Name}}) where N + node_indices = Vector{Node}() + for node in nodes + j = findfirst(i -> i == node, diagram.Names) + if isnothing(j) + throw(DomainError("Node $node does not exist.")) + end + push!(node_indices, j) + end + + path_set = Set{Path}() + for s in paths + s_states = Vector{State}() + for (i, s_i) in enumerate(s) + s_i_index = findfirst(x -> x == s_i, diagram.States[node_indices[i]]) + if isnothing(s_i_index) + throw(DomainError("Node $(nodes[i]) does not have a state called $s_i.")) + end + + push!(s_states, s_i_index) + end + push!(path_set, Path(s_states)) + end + + return ForbiddenPath((node_indices, path_set)) +end + +""" + function ForbiddenPath(diagram::InfluenceDiagram, nodes::Vector{Name}, paths::Vector{NTuple{N, Name}}) where N + +FixedPath outer construction function. Create FixedPath variable. + +# Arguments +- `diagram::InfluenceDiagram`: Influence diagram structure +- `fixed::Dict{Name, Name}`: Dictionary of nodes and their fixed states. Order is node=>state, and both are idefied with their names. + +# Example +```julia +julia> FixedPath(diagram, Dict("R1"=>"high", "R2"=>"high")) +``` +""" +function FixedPath(diagram::InfluenceDiagram, fixed::Dict{Name, Name}) + fixed_paths = Dict{Node, State}() + + for (j, s_j) in fixed + j_index = findfirst(i -> i == j, diagram.Names) + if isnothing(j_index) + throw(DomainError("Node $j does not exist.")) + end + + s_j_index = findfirst(s -> s == s_j, diagram.States[j_index]) + if isnothing(s_j_index) + throw(DomainError("Node $j does not have a state called $s_j.")) + end + push!(fixed_paths, Node(j_index) => State(s_j_index)) + end + + return FixedPath(fixed_paths) +end + + # --- Local Decision Strategy --- """ From 903da809157102b58c12571100bf9e56a59d2ce8 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 27 Aug 2021 16:34:12 +0300 Subject: [PATCH 071/133] Fixed CompatiblePaths inner construction function. --- src/analysis.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/analysis.jl b/src/analysis.jl index 4d7b208e..17047e6e 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -4,11 +4,11 @@ struct CompatiblePaths C::Vector{Node} Z::DecisionStrategy fixed::FixedPath - function CompatiblePaths(diagram, Z, fixed) - if !all(k∈Set(diagram.C) for k in keys(fixed)) + function CompatiblePaths(S, C, Z, fixed) + if !all(k∈Set(C) for k in keys(fixed)) throw(DomainError("You can only fix chance states.")) end - new(diagram.S, diagram.C, Z, fixed) + new(S, C, Z, fixed) end end @@ -28,8 +28,8 @@ for s in CompatiblePaths(diagram, Z) end ``` """ -function CompatiblePaths(diagram::InfluenceDiagram, Z::DecisionStrategy) - CompatiblePaths(diagram, Z, Dict{Node, State}()) +function CompatiblePaths(diagram::InfluenceDiagram, Z::DecisionStrategy, fixed::FixedPath=Dict{Node, State}()) + CompatiblePaths(diagram.S, diagram.C, Z, fixed) end function compatible_path(S::States, C::Vector{Node}, Z::DecisionStrategy, s_C::Path) From 510f1b6caf8ab9c9aa40ade4b0deab0293f81951 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 27 Aug 2021 16:34:44 +0300 Subject: [PATCH 072/133] Included new types and reorganised according to placement in code. --- src/DecisionProgramming.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/DecisionProgramming.jl b/src/DecisionProgramming.jl index 34d60a37..31efeb99 100644 --- a/src/DecisionProgramming.jl +++ b/src/DecisionProgramming.jl @@ -16,6 +16,8 @@ export Node, States, Path, paths, + ForbiddenPath, + FixedPath, Probabilities, Utility, Utilities, @@ -23,8 +25,6 @@ export Node, DefaultPathProbability, AbstractPathUtility, DefaultPathUtility, - LocalDecisionStrategy, - DecisionStrategy, validate_influence_diagram, InfluenceDiagram, generate_arcs!, @@ -35,14 +35,14 @@ export Node, add_probabilities!, UtilityMatrix, set_utility!, - add_utilities! + add_utilities!, + LocalDecisionStrategy, + DecisionStrategy export DecisionVariables, PathCompatibilityVariables, - ForbiddenPath, lazy_probability_cut, expected_value, - value_at_risk, conditional_value_at_risk export random_diagram From 1271814fb59b68f2d41266474925c681db2466fe Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 27 Aug 2021 16:55:39 +0300 Subject: [PATCH 073/133] Fixed docstring in analysis.jl --- src/analysis.jl | 52 ++++++++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/src/analysis.jl b/src/analysis.jl index 17047e6e..0ac2b431 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -1,4 +1,13 @@ +""" + struct CompatiblePaths + S::States + C::Vector{Node} + Z::DecisionStrategy + fixed::FixedPath + end +CompatiblePaths type. +""" struct CompatiblePaths S::States C::Vector{Node} @@ -13,9 +22,9 @@ struct CompatiblePaths end """ - CompatiblePaths(diagram::InfluenceDiagram, Z::DecisionStrategy) + CompatiblePaths(diagram::InfluenceDiagram, Z::DecisionStrategy, fixed::FixedPath=Dict{Node, State}()) -Interface for iterating over paths that are compatible and active given influence diagram and decision strategy. +CompatiblePaths outer construction function. Interface for iterating over paths that are compatible and active given influence diagram and decision strategy. 1) Initialize path `s` of length `n` 2) Fill chance states `s[C]` by generating subpaths `paths(C)` @@ -23,7 +32,7 @@ Interface for iterating over paths that are compatible and active given influenc # Examples ```julia -for s in CompatiblePaths(diagram, Z) +julia> for s in CompatiblePaths(diagram, Z) ... end ``` @@ -71,7 +80,10 @@ Base.eltype(::Type{CompatiblePaths}) = Path Base.length(S_Z::CompatiblePaths) = prod(S_Z.S[c] for c in S_Z.C) """ - UtilityDistribution + struct UtilityDistribution + u::Vector{Float64} + p::Vector{Float64} + end UtilityDistribution type. @@ -82,13 +94,13 @@ struct UtilityDistribution end """ - UtilityDistribution(S::States, P::AbstractPathProbability, U::AbstractPathUtility, Z::DecisionStrategy) + UtilityDistribution(diagram::InfluenceDiagram, Z::DecisionStrategy) -Constructs the probability mass function for path utilities on paths that are compatible and active. +Construct the probability mass function for path utilities on paths that are compatible with given decision strategy. # Examples ```julia -UtilityDistribution(S, P, U, Z) +julia> UtilityDistribution(diagram, Z) ``` """ function UtilityDistribution(diagram::InfluenceDiagram, Z::DecisionStrategy) @@ -142,19 +154,19 @@ struct StateProbabilities end """ - function StateProbabilities(S::States, P::AbstractPathProbability, Z::DecisionStrategy, node::Node, state::State, prev::StateProbabilities) + StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy, node::Name, state::Name, prior_probabilities::StateProbabilities) -Associates each node with array of conditional probabilities for each of its states occuring in compatible paths given fixed states and prior probability. +Associate each node with array of conditional probabilities for each of its states occuring in compatible paths given fixed states and prior probability. # Examples ```julia # Prior probabilities -prev = StateProbabilities(S, P, Z) +julia> prior_probabilities = StateProbabilities(diagram, Z) # Select node and fix its state -node = 1 -state = 2 -StateProbabilities(S, P, Z, node, state, prev) +julia> node = "R" +julia> state = "no test" +julia> StateProbabilities(diagram, Z, node, state, prior_probabilities) ``` """ function StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy, node::Name, state::Name, prior_probabilities::StateProbabilities) @@ -172,13 +184,13 @@ function StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy, node end """ - function StateProbabilities(S::States, P::AbstractPathProbability, Z::DecisionStrategy) + StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy) -Associates each node with array of probabilities for each of its states occuring in compatible paths. +Associate each node with array of probabilities for each of its states occuring in compatible paths. # Examples ```julia -StateProbabilities(S, P, Z) +julia> StateProbabilities(diagram, Z) ``` """ function StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy) @@ -190,9 +202,9 @@ function StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy) end """ - function value_at_risk(u::Vector{Float64}, p::Vector{Float64}, α::Float64) + value_at_risk(U_distribution::UtilityDistribution, α::Float64) -Value-at-risk. +Calculate value-at-risk. """ function value_at_risk(U_distribution::UtilityDistribution, α::Float64) @assert 0 ≤ α ≤ 1 "We should have 0 ≤ α ≤ 1." @@ -203,9 +215,9 @@ function value_at_risk(U_distribution::UtilityDistribution, α::Float64) end """ - function conditional_value_at_risk(u::Vector{Float64}, p::Vector{Float64}, α::Float64) + conditional_value_at_risk(u::Vector{Float64}, p::Vector{Float64}, α::Float64) -Conditional value-at-risk. +Calculate conditional value-at-risk. """ function conditional_value_at_risk(U_distribution::UtilityDistribution, α::Float64) x_α = value_at_risk(U_distribution, α) From 9204c58254b0fb9f287def82932d5bb6a1ef2249 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Mon, 30 Aug 2021 06:47:47 +0300 Subject: [PATCH 074/133] Fixed docstrings in decision_model.jl --- src/decision_model.jl | 50 +++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/src/decision_model.jl b/src/decision_model.jl index 7c219fbe..3ce8a89a 100644 --- a/src/decision_model.jl +++ b/src/decision_model.jl @@ -21,21 +21,20 @@ struct DecisionVariables end """ - DecisionVariables(model::Model, S::States, D::Vector{DecisionNode}; names::Bool=false, name::String="z") + DecisionVariables(model::Model, diagram::InfluenceDiagram; names::Bool=false, name::String="z") Create decision variables and constraints. # Arguments - `model::Model`: JuMP model into which variables are added. -- `S::States`: States structure associated with the influence diagram. -- `D::Vector{DecisionNode}`: Vector containing decicion nodes. +- `diagram::InfluenceDiagram`: Influence diagram structure. - `names::Bool`: Use names or have JuMP variables be anonymous. - `name::String`: Prefix for predefined decision variable naming convention. # Examples ```julia -z = DecisionVariables(model, S, D) +julia> z = DecisionVariables(model, diagram) ``` """ function DecisionVariables(model::Model, diagram::InfluenceDiagram; names::Bool=false, name::String="z") @@ -92,9 +91,8 @@ end """ PathCompatibilityVariables(model::Model, - z::DecisionVariables, - S::States, - P::AbstractPathProbability; + diagram::InfluenceDiagram, + z::DecisionVariables; names::Bool=false, name::String="x", forbidden_paths::Vector{ForbiddenPath}=ForbiddenPath[], @@ -105,10 +103,8 @@ Create path compatibility variables and constraints. # Arguments - `model::Model`: JuMP model into which variables are added. +- `diagram::InfluenceDiagram`: Influence diagram structure. - `z::DecisionVariables`: Decision variables from `DecisionVariables` function. -- `S::States`: States structure associated with the influence diagram. -- `P::AbstractPathProbability`: Path probabilities structure for which the function - `P(s)` is defined and returns the path probabilities for path `s`. - `names::Bool`: Use names or have JuMP variables be anonymous. - `name::String`: Prefix for predefined decision variable naming convention. - `forbidden_paths::Vector{ForbiddenPath}`: The forbidden subpath structures. @@ -120,7 +116,7 @@ Create path compatibility variables and constraints. # Examples ```julia -x_s = PathCompatibilityVariables(model, z, S, P; probability_cut = false) +julia> x_s = PathCompatibilityVariables(model, diagram; probability_cut = false) ``` """ function PathCompatibilityVariables(model::Model, @@ -159,13 +155,13 @@ function PathCompatibilityVariables(model::Model, end """ - lazy_probability_cut(model::Model, x_s::PathCompatibilityVariables, P::AbstractPathProbability) + lazy_probability_cut(model::Model, diagram::InfluenceDiagram, x_s::PathCompatibilityVariables) -Adds a probability cut to the model as a lazy constraint. +Add a probability cut to the model as a lazy constraint. # Examples ```julia -lazy_probability_cut(model, x_s, P) +julia> lazy_probability_cut(model, diagram, x_s) ``` !!! note @@ -190,25 +186,22 @@ end """ expected_value(model::Model, - x_s::PathCompatibilityVariables, - U::AbstractPathUtility, - P::AbstractPathProbability; + diagram::InfluenceDiagram, + x_s::PathCompatibilityVariables; probability_scale_factor::Float64=1.0) Create an expected value objective. # Arguments - `model::Model`: JuMP model into which variables are added. +- `diagram::InfluenceDiagram`: Influence diagram structure. - `x_s::PathCompatibilityVariables`: Path compatibility variables. -- `S::States`: States structure associated with the influence diagram. -- `P::AbstractPathProbability`: Path probabilities structure for which the function - `P(s)` is defined and returns the path probabilities for path `s`. - `probability_scale_factor::Float64`: Multiplies the path probabilities by this factor. # Examples ```julia -EV = expected_value(model, x_s, U, P) -EV = expected_value(model, x_s, U, P; probability_scale_factor = 10.0) +julia> EV = expected_value(model, diagram, x_s) +julia> EV = expected_value(model, diagram, x_s; probability_scale_factor = 10.0) ``` """ function expected_value(model::Model, @@ -225,9 +218,8 @@ end """ conditional_value_at_risk(model::Model, + diagram, x_s::PathCompatibilityVariables{N}, - U::AbstractPathUtility, - P::AbstractPathProbability, α::Float64; probability_scale_factor::Float64=1.0) where N @@ -235,10 +227,8 @@ Create a conditional value-at-risk (CVaR) objective. # Arguments - `model::Model`: JuMP model into which variables are added. +- `diagram::InfluenceDiagram`: Influence diagram structure. - `x_s::PathCompatibilityVariables`: Path compatibility variables. -- `S::States`: States structure associated with the influence diagram. -- `P::AbstractPathProbability`: Path probabilities structure for which the function - `P(s)` is defined and returns the path probabilities for path `s`. - `α::Float64`: Probability level at which conditional value-at-risk is optimised. - `probability_scale_factor::Float64`: Adjusts conditional value at risk model to be compatible with the expected value expression if the probabilities were scaled there. @@ -247,9 +237,9 @@ Create a conditional value-at-risk (CVaR) objective. # Examples ```julia -α = 0.05 # Parameter such that 0 ≤ α ≤ 1 -CVaR = conditional_value_at_risk(model, x_s, U, P, α) -CVaR = conditional_value_at_risk(model, x_s, U, P, α; probability_scale_factor = 10.0) +julia> α = 0.05 # Parameter such that 0 ≤ α ≤ 1 +julia> CVaR = conditional_value_at_risk(model, x_s, U, P, α) +julia> CVaR = conditional_value_at_risk(model, x_s, U, P, α; probability_scale_factor = 10.0) ``` """ function conditional_value_at_risk(model::Model, From bc14087b4ca93c6f4c7167bf781d19c1f631f0e7 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Mon, 30 Aug 2021 07:19:02 +0300 Subject: [PATCH 075/133] Fixed docstrings in printing.jl --- src/printing.jl | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/printing.jl b/src/printing.jl index 9a22dfb4..59b3c2ab 100644 --- a/src/printing.jl +++ b/src/printing.jl @@ -2,13 +2,13 @@ using DataFrames, PrettyTables using StatsBase, StatsBase.Statistics """ - function print_decision_strategy(S::States, Z::DecisionStrategy) + print_decision_strategy(diagram::InfluenceDiagram, Z::DecisionStrategy, state_probabilities::StateProbabilities; show_incompatible_states = false) Print decision strategy. # Examples ```julia -print_decision_strategy(S, Z) +>julia print_decision_strategy(diagram, Z, S_probabilities) ``` """ function print_decision_strategy(diagram::InfluenceDiagram, Z::DecisionStrategy, state_probabilities::StateProbabilities; show_incompatible_states = false) @@ -35,18 +35,18 @@ function print_decision_strategy(diagram::InfluenceDiagram, Z::DecisionStrategy, end """ - function print_utility_distribution(udist::UtilityDistribution; util_fmt="%f", prob_fmt="%f") + print_utility_distribution(U_distribution::UtilityDistribution; util_fmt="%f", prob_fmt="%f") Print utility distribution. # Examples ```julia -udist = UtilityDistribution(S, P, U, Z) -print_utility_distribution(udist) +>julia U_distribution = UtilityDistribution(diagram, Z) +>julia print_utility_distribution(U_distribution) ``` """ -function print_utility_distribution(udist::UtilityDistribution; util_fmt="%f", prob_fmt="%f") - df = DataFrame(Utility = udist.u, Probability = udist.p) +function print_utility_distribution(U_distribution::UtilityDistribution; util_fmt="%f", prob_fmt="%f") + df = DataFrame(Utility = U_distribution.u, Probability = U_distribution.p) formatters = ( ft_printf(util_fmt, [1]), ft_printf(prob_fmt, [2])) @@ -54,19 +54,19 @@ function print_utility_distribution(udist::UtilityDistribution; util_fmt="%f", p end """ - function print_state_probabilities(sprobs::StateProbabilities, nodes::Vector{Node}; prob_fmt="%f") + print_state_probabilities(sprobs::StateProbabilities, nodes::Vector{Node}; prob_fmt="%f") Print state probabilities with fixed states. # Examples ```julia -sprobs = StateProbabilities(S, P, U, Z) -print_state_probabilities(sprobs, [c.j for c in C]) -print_state_probabilities(sprobs, [d.j for d in D]) +>julia S_probabilities = StateProbabilities(diagram, Z) +>julia print_state_probabilities(S_probabilities, ["R"]) +>julia print_state_probabilities(S_probabilities, ["A"]) ``` """ function print_state_probabilities(diagram::InfluenceDiagram, state_probabilities::StateProbabilities, nodes::Vector{Name}; prob_fmt="%f") - node_indices = [findfirst(j -> j ==node, diagram.Names) for node in nodes] + node_indices = [findfirst(j -> j==node, diagram.Names) for node in nodes] probs = state_probabilities.probs fixed = state_probabilities.fixed @@ -87,13 +87,13 @@ function print_state_probabilities(diagram::InfluenceDiagram, state_probabilitie end """ -function print_statistics(udist::UtilityDistribution; fmt = "%f") + print_statistics(U_distribution::UtilityDistribution; fmt = "%f") Print statistics about utility distribution. """ -function print_statistics(udist::UtilityDistribution; fmt = "%f") - u = udist.u - w = ProbabilityWeights(udist.p) +function print_statistics(U_distribution::UtilityDistribution; fmt = "%f") + u = U_distribution.u + w = ProbabilityWeights(U_distribution.p) names = ["Mean", "Std", "Skewness", "Kurtosis"] statistics = [mean(u, w), std(u, w, corrected=false), skewness(u, w), kurtosis(u, w)] df = DataFrame(Name = names, Statistics = statistics) @@ -101,12 +101,12 @@ function print_statistics(udist::UtilityDistribution; fmt = "%f") end """ - function print_risk_measures(udist::UtilityDistribution, αs::Vector{Float64}; fmt = "%f") + print_risk_measures(U_distribution::UtilityDistribution, αs::Vector{Float64}; fmt = "%f") Print risk measures. """ -function print_risk_measures(udist::UtilityDistribution, αs::Vector{Float64}; fmt = "%f") - u, p = udist.u, udist.p +function print_risk_measures(U_distribution::UtilityDistribution, αs::Vector{Float64}; fmt = "%f") + u, p = U_distribution.u, U_distribution.p VaR = [value_at_risk(u, p, α) for α in αs] CVaR = [conditional_value_at_risk(u, p, α) for α in αs] df = DataFrame(α = αs, VaR = VaR, CVaR = CVaR) From 61708577a4dcc562cbb3f2a4008b6c247bd502a6 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Mon, 30 Aug 2021 08:15:37 +0300 Subject: [PATCH 076/133] Fixed docstrings in influence_diagram.jl --- src/influence_diagram.jl | 325 ++++++++++++++++++++++++++++++++------- 1 file changed, 267 insertions(+), 58 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index c391f9b6..2500c361 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -6,7 +6,7 @@ using Base.Iterators: product """ Node = Int16 -Primitive type for node index. Alias for `Int`. +Primitive type for node index. Alias for `Int16`. """ const Node = Int16 @@ -63,13 +63,13 @@ const State = Int """ - States <: AbstractArray{State, 1} + struct States <: AbstractArray{State, 1} States type. Works like `Vector{State}`. # Examples ```julia -S = States([2, 3, 2, 4]) +julia> S = States([2, 3, 2, 4]) ``` """ struct States <: AbstractArray{State, 1} @@ -99,6 +99,37 @@ Path type. Alias for `NTuple{N, State} where N`. """ const Path{N} = NTuple{N, State} where N + +""" + const ForbiddenPath = Tuple{Vector{Node}, Set{Path}} + +ForbiddenPath type. + +# Examples +```julia +julia> ForbiddenPath(([1, 2], Set([(1, 2)]))) +julia> ForbiddenPath[ + ([1, 2], Set([(1, 2)])), + ([3, 4, 5], Set([(1, 2, 3), (3, 4, 5)])) +] +``` +""" +const ForbiddenPath = Tuple{Vector{Node}, Set{Path}} + + +""" + const FixedPath = Dict{Node, State} + +FixedPath type. + +# Examples +```julia +julia> FixedPath(Dict(1=>1, 2=>3)) +``` +""" +const FixedPath = Dict{Node, State} + + """ function paths(states::AbstractVector{State}) @@ -116,18 +147,20 @@ function paths(states::AbstractVector{State}) end """ - function paths(states::AbstractVector{State}, fixed::Dict{Node, State}) + function paths(states::AbstractVector{State}, fixed::FixedPath) Iterate over paths with fixed states in lexicographical order. # Examples ```julia-repl julia> states = States([2, 3]) -julia> vec(collect(paths(states, Dict(1=>2)))) +julia> vec(collect(paths(states, Dict(Node(1) => State(2))))) [(2, 1), (2, 2), (2, 3)] + +julia> vec(collect(paths(states, FixedPath(diagram, Dict("O" => "lemon"))))) ``` """ -function paths(states::AbstractVector{State}, fixed::Dict{Node, State}) +function paths(states::AbstractVector{State}, fixed::FixedPath) iters = collect(UnitRange.(one(eltype(states)), states)) for (i, v) in fixed iters[i] = UnitRange(v, v) @@ -135,36 +168,6 @@ function paths(states::AbstractVector{State}, fixed::Dict{Node, State}) product(iters...) end -""" - const ForbiddenPath = Tuple{Vector{Node}, Set{Path}} - -ForbiddenPath type. - -# Examples -```julia -julia> ForbiddenPath(([1, 2], Set([(1, 2)]))) -julia> ForbiddenPath[ - ([1, 2], Set([(1, 2)])), - ([3, 4, 5], Set([(1, 2, 3), (3, 4, 5)])) -] -``` -""" -const ForbiddenPath = Tuple{Vector{Node}, Set{Path}} - - -""" - const FixedPath = Dict{Node, State} - -FixedPath type. - -# Examples -```julia -julia> FixedPath(Dict(1=>1, 2=>3)) -] -``` -""" -const FixedPath = Dict{Node, State} - # --- Probabilities --- @@ -176,7 +179,7 @@ Construct and validate stage probabilities. # Examples ```julia-repl julia> data = [0.5 0.5 ; 0.2 0.8] -julia> X = Probabilities(2, data) +julia> X = Probabilities(Node(2), data) julia> s = (1, 2) julia> X(s) 0.5 @@ -212,26 +215,26 @@ Abstract path probability type. # Examples ```julia -struct PathProbability <: AbstractPathProbability - C::Vector{ChanceNode} - # ... +julia> struct PathProbability <: AbstractPathProbability + C::Vector{ChanceNode} + # ... end -(U::PathProbability)(s::Path) = ... +julia> (P::PathProbability)(s::Path) = ... ``` """ abstract type AbstractPathProbability end """ - DefaultPathProbability <: AbstractPathProbability + struct DefaultPathProbability <: AbstractPathProbability Path probability. # Examples ```julia -P = DefaultPathProbability(C, X) -s = (1, 2) -P(s) +julia> P = DefaultPathProbability(diagram.C, diagram.X) +julia> s = (1, 2) +julia> P(s) ``` """ struct DefaultPathProbability <: AbstractPathProbability @@ -256,14 +259,14 @@ end # --- Utilities --- """ - Utility = Float32 + const Utility = Float32 Primitive type for utility. Alias for `Float32`. """ const Utility = Float32 """ - Utilities{N} <: AbstractArray{Utility, N} + struct Utilities{N} <: AbstractArray{Utility, N} State utilities. @@ -304,29 +307,30 @@ Abstract path utility type. # Examples ```julia -struct PathUtility <: AbstractPathUtility - V::Vector{ValueNode} - # ... -end +julia> struct PathUtility <: AbstractPathUtility + V::Vector{ValueNode} + # ... + end -(U::PathUtility)(s::Path) = ... +julia> (U::PathUtility)(s::Path) = ... +julia> (U::PathUtility)(s::Path, translation::Utility) = ... ``` """ abstract type AbstractPathUtility end """ - DefaultPathUtility <: AbstractPathUtility + struct DefaultPathUtility <: AbstractPathUtility Default path utility. # Examples ```julia -U = DefaultPathUtility(V, Y) -s = (1, 2) -U(s) +julia> U = DefaultPathUtility(V, Y) +julia> s = (1, 2) +julia> U(s) -t = -100.0 -U(s, t) +julia> t = -100.0 +julia> U(s, t) ``` """ struct DefaultPathUtility <: AbstractPathUtility @@ -343,8 +347,52 @@ function (U::DefaultPathUtility)(s::Path, t::Utility) end # --- Influence diagram --- +""" + mutable struct InfluenceDiagram + Nodes::Vector{AbstractNode} + Names::Vector{Name} + I_j::Vector{Vector{Node}} + States::Vector{Vector{Name}} + S::States + C::Vector{Node} + D::Vector{Node} + V::Vector{Node} + X::Vector{Probabilities} + Y::Vector{Utilities} + P::AbstractPathProbability + U::AbstractPathUtility + translation::Utility + function InfluenceDiagram() + new(Vector{AbstractNode}()) + end + end + +Hold all information related to the influence diagram. + +# Fields +- `Nodes::Vector{AbstractNode}`: Vector of added abstract nodes. +- `Names::Vector{Name}`: Names of nodes in order of their indices. +- `I_j::Vector{Vector{Node}}`: Information sets of nodes in order of their indices. + Nodes of information sets identified by their indices. +- `States::Vector{Vector{Name}}`: States of each node in order of their indices. +- `S::States`: Vector showing the number of states each node has. +- `C::Vector{Node}`: Indices of chance nodes in ascending order. +- `D::Vector{Node}`: Indices of decision nodes in ascending order. +- `V::Vector{Node}`: Indices of value nodes in ascending order. +- `X::Vector{Probabilities}`: Probability matrices of chance nodes in order of chance + nodes in C. +- `Y::Vector{Utilities}`: Utility matrices of value nodes in order of value nodes in V. +- `P::AbstractPathProbability`: Path probabilities. +- `U::AbstractPathUtility`: Path utilities. +- `translation::Utility`: Utility translation for storing the positive or negative + utility translation. +# Examples +```julia +julia> diagram = InfluenceDiagram() +``` +""" mutable struct InfluenceDiagram Nodes::Vector{AbstractNode} Names::Vector{Name} @@ -398,6 +446,16 @@ function validate_node(diagram::InfluenceDiagram, end end +""" + function add_node!(diagram::InfluenceDiagram, node::AbstractNode) + +Add node to influence diagram structure. + +# Examples +```julia +julia> add_node!(diagram, ChanceNode("O", [], ["lemon", "peach"])) +``` +""" function add_node!(diagram::InfluenceDiagram, node::AbstractNode) if !isa(node, ValueNode) validate_node(diagram, node.name, node.I_j, states = node.states) @@ -409,7 +467,15 @@ end # --- Adding Probabilities --- +""" + struct ProbabilityMatrix{N} <: AbstractArray{Float64, N} + nodes::Vector{Name} + indices::Vector{Dict{Name, Int}} + matrix::Array{Float64, N} + end +Construct probability matrix. +""" struct ProbabilityMatrix{N} <: AbstractArray{Float64, N} nodes::Vector{Name} indices::Vector{Dict{Name, Int}} @@ -421,7 +487,16 @@ Base.getindex(PM::ProbabilityMatrix, I::Vararg{Int,N}) where N = getindex(PM.mat Base.setindex!(PM::ProbabilityMatrix, p::Float64, I::Vararg{Int,N}) where N = (PM.matrix[I...] = p) Base.setindex!(PM::ProbabilityMatrix{N}, X, I::Vararg{Any, N}) where N = (PM.matrix[I...] .= X) +""" + function ProbabilityMatrix(diagram::InfluenceDiagram, node::Name) + +Initialise a probability matrix for a given chance node. +# Examples +```julia +julia> X_O = ProbabilityMatrix(diagram, "O") +``` +""" function ProbabilityMatrix(diagram::InfluenceDiagram, node::Name) if node ∉ diagram.Names throw(DomainError("Node $node should be added as a node to the influence diagram.")) @@ -447,6 +522,18 @@ function ProbabilityMatrix(diagram::InfluenceDiagram, node::Name) return ProbabilityMatrix(names, indices, matrix) end +""" + function set_probability!(probability_matrix::ProbabilityMatrix, scenario::Array{String}, probability::Float64) + +Set a single probability value into probability matrix. + +# Examples +```julia +julia> X_O = ProbabilityMatrix(diagram, "O") +julia> set_probability!(X_O, ["peach"], 0.8) +julia> set_probability!(X_O, ["lemon"], 0.2) +``` +""" function set_probability!(probability_matrix::ProbabilityMatrix, scenario::Array{String}, probability::Float64) index = Vector{Int}() for (i, s) in enumerate(scenario) @@ -460,6 +547,17 @@ function set_probability!(probability_matrix::ProbabilityMatrix, scenario::Array probability_matrix[index...] = probability end +""" + function set_probability!(probability_matrix::ProbabilityMatrix, scenario::Array{Any}, probabilities::Array{Float64}) + +Set multiple probability values into probability matrix. + +# Examples +```julia +julia> X_O = ProbabilityMatrix(diagram, "O") +julia> set_probability!(X_O, ["lemon", "peach"], [0.2, 0.8]) +``` +""" function set_probability!(probability_matrix::ProbabilityMatrix, scenario::Array{Any}, probabilities::Array{Float64}) index = Vector{Any}() for (i, s) in enumerate(scenario) @@ -475,7 +573,20 @@ function set_probability!(probability_matrix::ProbabilityMatrix, scenario::Array probability_matrix[index...] = probabilities end +""" + function add_probabilities!(diagram::InfluenceDiagram, node::Name, probabilities::AbstractArray{Float64, N}) where N + +Add probability matrix to influence diagram, specifically to its X vector. + +# Examples +```julia +julia> X_O = ProbabilityMatrix(diagram, "O") +julia> set_probability!(X_O, ["lemon", "peach"], [0.2, 0.8]) +julia> add_probabilities!(diagram, "O", X_O) +julia> add_probabilities!(diagram, "O", [0.2, 0.8]) +``` +""" function add_probabilities!(diagram::InfluenceDiagram, node::Name, probabilities::AbstractArray{Float64, N}) where N c = findfirst(x -> x==node, diagram.Names) @@ -498,6 +609,15 @@ end # --- Adding Utilities --- +""" + struct UtilityMatrix{N} <: AbstractArray{Utility, N} + I_v::Vector{Name} + indices::Vector{Dict{Name, Int}} + matrix::Array{Utility, N} + end + +Construct utility matrix. +""" struct UtilityMatrix{N} <: AbstractArray{Utility, N} I_v::Vector{Name} indices::Vector{Dict{Name, Int}} @@ -509,7 +629,16 @@ Base.getindex(UM::UtilityMatrix, I::Vararg{Int,N}) where N = getindex(UM.matrix, Base.setindex!(UM::UtilityMatrix, y::T, I::Vararg{Int,N}) where {N, T<:Real} = (UM.matrix[I...] = y) Base.setindex!(UM::UtilityMatrix{N}, Y, I::Vararg{Any, N}) where N = (UM.matrix[I...] .= Y) +""" + function UtilityMatrix(diagram::InfluenceDiagram, node::Name) + +Initialise a utility matrix for a value node. +# Examples +```julia +julia> Y_V3 = UtilityMatrix(diagram, "V3") +``` +""" function UtilityMatrix(diagram::InfluenceDiagram, node::Name) if node ∉ diagram.Names throw(DomainError("Node $node should be added as a node to the influence diagram.")) @@ -535,6 +664,17 @@ function UtilityMatrix(diagram::InfluenceDiagram, node::Name) return UtilityMatrix(names, indices, matrix) end +""" + function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{String}, utility::Real) + +Set a single utility value into utility matrix. + +# Examples +```julia +julia> Y_V3 = UtilityMatrix(diagram, "V3") +julia> set_utility!(Y_V3, ["lemon", "buy without guarantee"], -200) +``` +""" function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{String}, utility::Real) index = Vector{Int}() for (i, s) in enumerate(scenario) @@ -548,6 +688,17 @@ function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{String}, ut utility_matrix[index...] = utility end +""" + function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{Any}, utility::Array{T}) where T<:Real + +Set multiple utility values into utility matrix. + +# Examples +```julia +julia> Y_V3 = UtilityMatrix(diagram, "V3") +julia> set_utility!(Y_V3, ["peach", :], [-40, -20, 0]) +``` +""" function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{Any}, utility::Array{T}) where T<:Real index = Vector{Any}() for (i, s) in enumerate(scenario) @@ -563,6 +714,22 @@ function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{Any}, utili utility_matrix[index...] = utility end + +""" + function add_utilities!(diagram::InfluenceDiagram, node::Name, utilities::AbstractArray{T, N}) where {N,T<:Real} + +Add utility matrix to influence diagram, specifically to its Y vector. + +# Examples +```julia +julia> Y_V3 = UtilityMatrix(diagram, "V3") +julia> set_utility!(Y_V3, ["peach", :], [-40, -20, 0]) +julia> set_utility!(Y_V3, ["lemon", :], [-200, 0, 0]) +julia> add_utilities!(diagram, "V3", Y_V3) + +julia> add_utilities!(diagram, "V1", [0, -25]) +``` +""" function add_utilities!(diagram::InfluenceDiagram, node::Name, utilities::AbstractArray{T, N}) where {N,T<:Real} v = findfirst(x -> x==node, diagram.Names) @@ -610,7 +777,20 @@ function validate_structure(Nodes::Vector{AbstractNode}, C_and_D::Vector{Abstrac end end +""" + function generate_arcs!(diagram::InfluenceDiagram) + +Generate arc structures using nodes added to influence diagram, by generating correct +values for the vectors Names, I_j, states, S, C, D, V in the influence digram. +# Examples +```julia +julia> generate_arcs!(diagram) +``` + +!!! note +The arcs must be generated before probabilities or utilities can be added to the influence diagram. +""" function generate_arcs!(diagram::InfluenceDiagram) # Chance and decision nodes @@ -695,7 +875,36 @@ end # --- Generating Diagram --- +""" +function generate_diagram!(diagram::InfluenceDiagram; + default_probability::Bool=true, + default_utility::Bool=true, + positive_path_utility::Bool=false, + negative_path_utility::Bool=false) + +Generate complete influence diagram with probabilities and utilities as well. + +# Arguments +- `default_probability::Bool=true`: Choice to use default path probabilities. +- `default_utility::Bool=true`: Choice to use default path utilities. +- `positive_path_utility::Bool=false`: Choice to use a positive path utility translation. +- `negative_path_utility::Bool=false`: Choice to use a negative path utility translation. + +# Examples +```julia +julia> generate_diagram!(diagram) +``` +!!! note +The influence diagram must be generated after probabilities and utilities are added +but before creating the decision model. + +!!! note +If the default probabilities and utilities are not used, define `AbstractPathProbability` +and `AbstractPathUtility` structures and define P(s), U(s) and U(s, t) functions +for them. Add the `AbstractPathProbability` and `AbstractPathUtility` structures +to the influence diagram fields P and U. +""" function generate_diagram!(diagram::InfluenceDiagram; default_probability::Bool=true, default_utility::Bool=true, From e15118883acd80f7e5b3962f1bd3db276f6a4a95 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Mon, 30 Aug 2021 08:37:23 +0300 Subject: [PATCH 077/133] Updated API reference. --- docs/src/api.md | 44 +++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index c04f7d19..002d88cf 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -1,25 +1,27 @@ # API Reference `DecisionProgramming.jl` API reference. + ## `influence_diagram.jl` ### Nodes ```@docs Node +Name +AbstractNode ChanceNode DecisionNode ValueNode State States -States(::Vector{Tuple{State, Vector{Node}}}) -validate_influence_diagram ``` ### Paths ```@docs Path -paths(::AbstractVector{State}) -paths(::AbstractVector{State}, ::Dict{Node, State}) ForbiddenPath +FixedPath +paths(::AbstractVector{State}) +paths(::AbstractVector{State}, FixedPath) ``` ### Probabilities @@ -33,9 +35,10 @@ AbstractPathProbability DefaultPathProbability ``` -### Consequences +### Utilities ```@docs -Consequences +Utility +Utilities ``` ### Path Utility @@ -44,6 +47,18 @@ AbstractPathUtility DefaultPathUtility ``` +### InfluenceDiagram +InfluenceDiagram +generate_arcs! +generate_diagram! +add_node! +ProbabilityMatrix +set_probability! +add_probabilities! +UtilityMatrix +set_utility! +add_utilities! + ### Decision Strategy ```@docs LocalDecisionStrategy @@ -61,10 +76,8 @@ lazy_probability_cut(::Model, ::PathCompatibilityVariables, ::AbstractPathProbab ### Objective Functions ```@docs -PositivePathUtility -NegativePathUtility -expected_value(::Model, ::PathCompatibilityVariables, ::AbstractPathUtility, ::AbstractPathProbability; ::Float64) -conditional_value_at_risk(::Model, ::PathCompatibilityVariables{N}, ::AbstractPathUtility, ::AbstractPathProbability, ::Float64; ::Float64) where N +expected_value(::Model, ::InfluenceDiagram, ::PathCompatibilityVariables; ::Float64) +conditional_value_at_risk(::Model, ::InfluenceDiagram, ::PathCompatibilityVariables{N}, ::Float64; ::Float64) where N ``` ### Decision Strategy from Variables @@ -76,13 +89,14 @@ DecisionStrategy(::DecisionVariables) ## `analysis.jl` ```@docs CompatiblePaths +CompatiblePaths(::InfluenceDiagram, ::DecisionStrategy, ::FixedPath) UtilityDistribution -UtilityDistribution(::States, ::AbstractPathProbability, ::AbstractPathUtility, ::DecisionStrategy) +UtilityDistribution(::InfluenceDiagram, ::DecisionStrategy) StateProbabilities -StateProbabilities(::States, ::AbstractPathProbability, ::DecisionStrategy) -StateProbabilities(::States, ::AbstractPathProbability, ::DecisionStrategy, ::Node, ::State, ::StateProbabilities) -value_at_risk(::Vector{Float64}, ::Vector{Float64}, ::Float64) -conditional_value_at_risk(::Vector{Float64}, ::Vector{Float64}, ::Float64) +StateProbabilities(::InfluenceDiagram, ::DecisionStrategy) +StateProbabilities(::InfluenceDiagram, ::DecisionStrategy, ::Name, ::Name, ::StateProbabilities) +value_at_risk(::UtilityDistribution, ::Float64) +conditional_value_at_risk(::UtilityDistribution, ::Float64) ``` ## `printing.jl` From 01afc38eb42ba685c8faaf0fa45aa8cab440db1a Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Mon, 30 Aug 2021 09:35:42 +0300 Subject: [PATCH 078/133] Added some printing to pig breeding. --- examples/pig_breeding.jl | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/examples/pig_breeding.jl b/examples/pig_breeding.jl index 83a78403..1584067f 100644 --- a/examples/pig_breeding.jl +++ b/examples/pig_breeding.jl @@ -80,19 +80,15 @@ print_utility_distribution(U_distribution) @info("Printing statistics") print_statistics(U_distribution) -#= @info("State probabilities:") -sprobs = StateProbabilities(diagram.S, diagram.P, Z) -print_state_probabilities(sprobs, health) -print_state_probabilities(sprobs, test) -print_state_probabilities(sprobs, treat) +print_state_probabilities(diagram, S_probabilities, [["H$i" for i in 1:N]...]) +print_state_probabilities(diagram, S_probabilities, [["T$i" for i in 1:N-1]...]) +print_state_probabilities(diagram, S_probabilities, [["D$i" for i in 1:N-1]...]) @info("Conditional state probabilities") -node = 1 -for state in 1:2 - sprobs2 = StateProbabilities(diagram.S, diagram.P, Z, node, state, sprobs) - print_state_probabilities(sprobs2, health) - print_state_probabilities(sprobs2, test) - print_state_probabilities(sprobs2, treat) +for state in ["ill", "healthy"] + S_probabilities2 = StateProbabilities(diagram, Z, "H1", state, S_probabilities) + print_state_probabilities(diagram, S_probabilities2, [["H$i" for i in 1:N]...]) + print_state_probabilities(diagram, S_probabilities2, [["T$i" for i in 1:N-1]...]) + print_state_probabilities(diagram, S_probabilities2, [["D$i" for i in 1:N-1]...]) end -=# From 073c6428c9131e95506225347a8017a8a2922430 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Mon, 30 Aug 2021 10:00:28 +0300 Subject: [PATCH 079/133] Updated used car buyer example and documentation. --- docs/src/examples/used-car-buyer.md | 180 +++++++++++++--------------- examples/used_car_buyer.jl | 24 ++-- 2 files changed, 102 insertions(+), 102 deletions(-) diff --git a/docs/src/examples/used-car-buyer.md b/docs/src/examples/used-car-buyer.md index f1a0a697..22171663 100644 --- a/docs/src/examples/used-car-buyer.md +++ b/docs/src/examples/used-car-buyer.md @@ -15,130 +15,128 @@ We now add two new features to the problem. A stranger approaches Joe and offers We present the new influence diagram above. The decision node $T$ denotes the decision to accept or decline the stranger's offer, and $R$ is the outcome of the test. We introduce new value nodes $V_1$ and $V_2$ to represent the testing costs and the base profit from purchasing the car. Additionally, the decision node $A$ now can choose to buy with a guarantee. +We start by defining the influence diagram structure. The nodes, as well as their information sets and states, are defined in the first block. Next, the influence diagram parameters consisting of the probabilities and utilities are defined. + + ```julia using JuMP, Gurobi using DecisionProgramming - -const O = 1 # Chance node: lemon or peach -const T = 2 # Decision node: pay stranger for advice -const R = 3 # Chance node: observation of state of the car -const A = 4 # Decision node: purchase alternative -const O_states = ["lemon", "peach"] -const T_states = ["no test", "test"] -const R_states = ["no test", "lemon", "peach"] -const A_states = ["buy without guarantee", "buy with guarantee", "don't buy"] - -S = States([ - (length(O_states), [O]), - (length(T_states), [T]), - (length(R_states), [R]), - (length(A_states), [A]), -]) -C = Vector{ChanceNode}() -D = Vector{DecisionNode}() -V = Vector{ValueNode}() -X = Vector{Probabilities}() -Y = Vector{Consequences}() -``` - -We start by defining the influence diagram structure. The decision and chance nodes, as well as their states, are defined in the first block. Next, the influence diagram parameters consisting of the node sets, probabilities, consequences and the state spaces of the nodes are defined. +diagram = InfluenceDiagram() +``` ### Car's State -The chance node $O$ is defined by its information set $I(O)$ and probability distribution $X_O$. As seen in the influence diagram, the information set is empty and the node is a root node. The probability distribution is thus simply defined over the two states of $O$. +The chance node $O$ is defined by its name, its information set $I(O)$ and its states $lemon$ and $peach$. As seen in the influence diagram, the information set is empty and the node is a root node. ```julia -I_O = Vector{Node}() -X_O = [0.2, 0.8] -push!(C, ChanceNode(O, I_O)) -push!(X, Probabilities(O, X_O)) +add_node!(diagram, ChanceNode("O", [], ["lemon", "peach"])) ``` ### Stranger's Offer Decision -A decision node is simply defined by its information state. +A decision node is also defined by its name, its information set and its states. ```julia -I_T = Vector{Node}() -push!(D, DecisionNode(T, I_T)) +add_node!(diagram, DecisionNode("T", [], ["no test", "test"])) ``` ### Test's Outcome -The second chance node, $R$, has nodes $O$ and $T$ in its information set, and the probabilities $ℙ(s_j∣𝐬_{I(j)})$ must thus be defined for all combinations of states in $O$, $T$ and $R$. +The second chance node, $R$, has nodes $O$ and $T$ in its information set, and three states describing the situations of no test being done, and the test declaring the car to be a lemon or a peach. ```julia -I_R = [O, T] -X_R = zeros(S[O], S[T], S[R]) -X_R[1, 1, :] = [1,0,0] -X_R[1, 2, :] = [0,1,0] -X_R[2, 1, :] = [1,0,0] -X_R[2, 2, :] = [0,0,1] -push!(C, ChanceNode(R, I_R)) -push!(X, Probabilities(R, X_R)) +add_node!(diagram, ChanceNode("R", ["O", "T"], ["no test", "lemon", "peach"])) ``` ### Purchace Decision +The purchase decision represented by node $A$ is added as follows. ```julia -I_A = [R] -push!(D, DecisionNode(A, I_A)) +add_node!(diagram, DecisionNode("A", ["R"], ["buy without guarantee", "buy with guarantee", "don't buy"])) ``` +### Testing fee, base profit and repair costs -### Testing Cost +Value nodes are defined by only their names and information sets because they do not have states. Instead, value nodes map their information states to utility values which will be added later on. +```julia +add_node!(diagram, ValueNode("V1", ["T"])) +add_node!(diagram, ValueNode("V2", ["A"])) +add_node!(diagram, ValueNode("V3", ["O", "A"])) +``` -We continue by defining the utilities (consequences) associated with value nodes. The value nodes are defined similarly as the chance nodes, except that instead of probabilities, we define consequences $Y_j(𝐬_{I(j)})$. Value nodes can be named just like the other nodes, e.g. $V1 = 5$, but considering that the index of value nodes is not needed elsewhere (value nodes can't be in information sets), we choose to simply use the index number when creating the node. +### Generate arcs +Now that all of the nodes have been added to our influence diagram we must generate the arcs. This step orders the nodes, gives them indices and reorganises the information into the appropriate form. +```julia +generate_arcs!(diagram) +``` +### Probabilities +We continue by defining the probability distributions for each chance node. + +Node $O$ is a root node and has two states thus, its probability distribution is simply defined over the two states. We can use the `ProbabilityMatrix` structure in creating the probability matrix easily without having to worry about the matrix dimentions. Then we add the probabilities to the influence diagram. ```julia -I_V1 = [T] -Y_V1 = [0.0, -25.0] -push!(V, ValueNode(5, I_V1)) -push!(Y, Consequences(5, Y_V1)) +X_O = ProbabilityMatrix(diagram, "O") +set_probability!(X_O, ["peach"], 0.8) +set_probability!(X_O, ["lemon"], 0.2) +add_probabilities!(diagram, "O", X_O) ``` -### Base Profit of Purchase +Node $R$ has two nodes in its information set and three states. the probabilities $P(s_j \mid s_{I(j)})$ must thus be defined for all combinations of states in $O$, $T$ and $R$. Since these nodes have 2, 3, and 3 states respectively, the probability matrix has 12 elements. We will set 3 probability values at a time to make this feat more swift. ```julia -I_V2 = [A] -Y_V2 = [100.0, 40.0, 0.0] -push!(V, ValueNode(6, I_V2)) -push!(Y, Consequences(6, Y_V2)) +X_R = ProbabilityMatrix(diagram, "R") +set_probability!(X_R, ["lemon", "no test", :], [1,0,0]) +set_probability!(X_R, ["lemon", "test", :], [0,1,0]) +set_probability!(X_R, ["peach", "no test", :], [1,0,0]) +set_probability!(X_R, ["peach", "test", :], [0,0,1]) +add_probabilities!(diagram, "R", X_R) ``` -### Repairing Cost -The rows of the consequence matrix Y_V3 correspond to the state of the car, while the columns correspond to the decision made in node $A$. +### Testing Cost +We continue by defining the utilities associated with value nodes. The utilities $Y_j(𝐬_{I(j)})$ are defined and added similarly to the probabilities. + +Value node $V1$ has only node $T$ in its information set and node $T$ only has two states. Therefore, node $V1$ needs to map exactly two utility values, on for state $tes$ and the other for $no test$. ```julia -I_V3 = [O, A] -Y_V3 = [-200.0 0.0 0.0; - -40.0 -20.0 0.0] -push!(V, ValueNode(7, I_V3)) -push!(Y, Consequences(7, Y_V3)) +Y_V1 = UtilityMatrix(diagram, "V1") +set_utility!(Y_V1, ["test"], -25) +set_utility!(Y_V1, ["no test"], 0) +add_utilities!(diagram, "V1", Y_V1) ``` -### Validating the Influence Diagram -Validate influence diagram and sort nodes, probabilities and consequences +### Base Profit of Purchase +```julia +Y_V2 = UtilityMatrix(diagram, "V2") +set_utility!(Y_V2, ["buy without guarantee"], 100) +set_utility!(Y_V2, ["buy with guarantee"], 40) +set_utility!(Y_V2, ["don't buy"], 0) +add_utilities!(diagram, "V2", Y_V2) +``` +### Repair Cost +The rows of the utilities matrix `Y_V3` correspond to the state of the car, while the columns correspond to the decision made in node $A$. The utilities can be added as follows. Notice that the utility values for the second row are added in one line, in this case it is important to give the utility values in the right order. The order of the columns is determined by the order in which the states are given when declaring node $A$. See the [usage page](../usage.md) for more on this more compact syntax. ```julia -validate_influence_diagram(S, C, D, V) -sort!.((C, D, V, X, Y), by = x -> x.j) +Y_V3 = UtilityMatrix(diagram, "V3") +set_utility!(Y_V3, ["lemon", "buy without guarantee"], -200) +set_utility!(Y_V3, ["lemon", "buy with guarantee"], 0) +set_utility!(Y_V3, ["lemon", "don't buy"], 0) +set_utility!(Y_V3, ["peach", :], [-40, -20, 0]) +add_utilities!(diagram, "V3", Y_V3) ``` -Default path probabilities and utilities are defined as the joint probability of all chance events in the diagram and the sum of utilities in value nodes, respectively. In the [Contingent Portfolio Programming](contingent-portfolio-programming.md) example, we show how to use a user-defined custom path utility function. +### Generate Influence Diagram +Finally, generate the full influence diagram before defining the decision model. By default this function uses the default path probabilities and utilities, which are defined as the joint probability of all chance events in the diagram and the sum of utilities in value nodes, respectively. In the [Contingent Portfolio Programming](contingent-portfolio-programming.md) example, we show how to use a user-defined custom path utility function. ```julia -P = DefaultPathProbability(C, X) -U = DefaultPathUtility(V, Y) +generate_diagram!(diagram) ``` - ## Decision Model We then construct the decision model using the DecisionProgramming.jl package, using the expected value as the objective. ```julia model = Model() -z = DecisionVariables(model, S, D) -x_s = PathCompatibilityVariables(model, z, S, P) -EV = expected_value(model, x_s, U, P) +z = DecisionVariables(model, diagram) +x_s = PathCompatibilityVariables(model, diagram, z) +EV = expected_value(model, diagram, x_s) @objective(model, Max, EV) ``` @@ -156,39 +154,33 @@ optimize!(model) ## Analyzing Results ### Decision Strategy -Once the model is solved, we obtain the following decision strategy: +Once the model is solved, we extract the results. The results are the decision strategy, state probabilities and utility distribution. ```julia Z = DecisionStrategy(z) +S_probabilities = StateProbabilities(diagram, Z) +U_distribution = UtilityDistribution(diagram, Z) ``` +We obtain the following optimal decision strategy: ```julia-repl julia> print_decision_strategy(S, Z) -┌────────┬────┬───┐ -│ Nodes │ () │ 2 │ -├────────┼────┼───┤ -│ States │ () │ 2 │ -└────────┴────┴───┘ -┌────────┬──────┬───┐ -│ Nodes │ (3,) │ 4 │ -├────────┼──────┼───┤ -│ States │ (1,) │ 3 │ -│ States │ (2,) │ 2 │ -│ States │ (3,) │ 1 │ -└────────┴──────┴───┘ +┌───────────────┐ +│ Decision in T │ +├───────────────┤ +│ test │ +└───────────────┘ +┌───────────────┬───────────────────────┐ +│ State(s) of R │ Decision in A │ +├───────────────┼───────────────────────┤ +│ lemon │ buy with guarantee │ +│ peach │ buy without guarantee │ +└───────────────┴───────────────────────┘ ``` -To start explaining this output, let's take a look at the top table. On the right, we have the decision node 2. We defined earlier that the node $T$ is node number 2. On the left, we have the information set of that decision node, which is empty. The strategy in the first decision node is to choose alternative 2, which we defined to be testing the car. - -In the bottom table, we have node number 4 (node $A$) and its predecessor, node number 3 (node $R$). The first row, where we obtain no test result, is invalid for this strategy since we tested the car. If the car is a lemon, Joe should buy the car with a guarantee (alternative 2), and if it is a peach, buy the car without guarantee (alternative 1). - ### Utility Distribution -```julia -udist = UtilityDistribution(S, P, U, Z) -``` - ```julia-repl -julia> print_utility_distribution(udist) +julia> print_utility_distribution(U_distribution) ┌───────────┬─────────────┐ │ Utility │ Probability │ │ Float64 │ Float64 │ diff --git a/examples/used_car_buyer.jl b/examples/used_car_buyer.jl index 350c73f8..ec188d33 100644 --- a/examples/used_car_buyer.jl +++ b/examples/used_car_buyer.jl @@ -7,9 +7,8 @@ using DecisionProgramming diagram = InfluenceDiagram() add_node!(diagram, ChanceNode("O", [], ["lemon", "peach"])) -add_node!(diagram, ChanceNode("R", ["O", "T"], ["no test", "lemon", "peach"])) - add_node!(diagram, DecisionNode("T", [], ["no test", "test"])) +add_node!(diagram, ChanceNode("R", ["O", "T"], ["no test", "lemon", "peach"])) add_node!(diagram, DecisionNode("A", ["R"], ["buy without guarantee", "buy with guarantee", "don't buy"])) add_node!(diagram, ValueNode("V1", ["T"])) @@ -25,14 +24,23 @@ add_probabilities!(diagram, "O", X_O) X_R = ProbabilityMatrix(diagram, "R") -X_R[1, 1, :] = [1,0,0] -X_R[1, 2, :] = [0,1,0] -X_R[2, 1, :] = [1,0,0] -X_R[2, 2, :] = [0,0,1] +set_probability!(X_R, ["lemon", "no test", :], [1,0,0]) +set_probability!(X_R, ["lemon", "test", :], [0,1,0]) +set_probability!(X_R, ["peach", "no test", :], [1,0,0]) +set_probability!(X_R, ["peach", "test", :], [0,0,1]) add_probabilities!(diagram, "R", X_R) -add_utilities!(diagram, "V1", [0, -25]) -add_utilities!(diagram, "V2", [100, 40, 0]) +Y_V1 = UtilityMatrix(diagram, "V1") +set_utility!(Y_V1, ["test"], -25) +set_utility!(Y_V1, ["no test"], 0) +add_utilities!(diagram, "V1", Y_V1) + + +Y_V2 = UtilityMatrix(diagram, "V2") +set_utility!(Y_V2, ["buy without guarantee"], 100) +set_utility!(Y_V2, ["buy with guarantee"], 40) +set_utility!(Y_V2, ["don't buy"], 0) +add_utilities!(diagram, "V2", Y_V2) Y_V3 = UtilityMatrix(diagram, "V3") set_utility!(Y_V3, ["lemon", "buy without guarantee"], -200) From 9fc896dd24fc0e57f0e9ac2dbd9bf2e181ec0023 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Mon, 30 Aug 2021 10:01:31 +0300 Subject: [PATCH 080/133] Made set_probability! accept Real numbers, the conversion happens in setindex --- src/influence_diagram.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 2500c361..aef9eee2 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -484,7 +484,7 @@ end Base.size(PM::ProbabilityMatrix) = size(PM.matrix) Base.getindex(PM::ProbabilityMatrix, I::Vararg{Int,N}) where N = getindex(PM.matrix, I...) -Base.setindex!(PM::ProbabilityMatrix, p::Float64, I::Vararg{Int,N}) where N = (PM.matrix[I...] = p) +Base.setindex!(PM::ProbabilityMatrix, p::T, I::Vararg{Int,N}) where {N, T<:Real} = (PM.matrix[I...] = p) Base.setindex!(PM::ProbabilityMatrix{N}, X, I::Vararg{Any, N}) where N = (PM.matrix[I...] .= X) """ @@ -534,7 +534,7 @@ julia> set_probability!(X_O, ["peach"], 0.8) julia> set_probability!(X_O, ["lemon"], 0.2) ``` """ -function set_probability!(probability_matrix::ProbabilityMatrix, scenario::Array{String}, probability::Float64) +function set_probability!(probability_matrix::ProbabilityMatrix, scenario::Array{String}, probability::Real) index = Vector{Int}() for (i, s) in enumerate(scenario) if get(probability_matrix.indices[i], s, 0) == 0 @@ -558,7 +558,7 @@ julia> X_O = ProbabilityMatrix(diagram, "O") julia> set_probability!(X_O, ["lemon", "peach"], [0.2, 0.8]) ``` """ -function set_probability!(probability_matrix::ProbabilityMatrix, scenario::Array{Any}, probabilities::Array{Float64}) +function set_probability!(probability_matrix::ProbabilityMatrix, scenario::Array{Any}, probabilities::Array{T}) where T<:Real index = Vector{Any}() for (i, s) in enumerate(scenario) if isa(s, Colon) From 91cc33886a99cf56b67e62607d6308aaa9c431f2 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Mon, 30 Aug 2021 13:17:19 +0300 Subject: [PATCH 081/133] Updated example pig breeding documentation. --- docs/src/examples/pig-breeding.md | 319 ++++++++++++++---------------- examples/pig_breeding.jl | 9 +- 2 files changed, 150 insertions(+), 178 deletions(-) diff --git a/docs/src/examples/pig-breeding.md b/docs/src/examples/pig-breeding.md index 446825ee..d1c5977c 100644 --- a/docs/src/examples/pig-breeding.md +++ b/docs/src/examples/pig-breeding.md @@ -16,42 +16,60 @@ The influence diagram for the the generalized $N$-month pig breeding. The nodes > The dashed arcs represent the no-forgetting principle and we can toggle them on and off in the formulation. -In decision programming, we start by defining the node indices and states, as follows: +In decision programming, we start by initialising an empty influence diagram. For this problem, we declare $N = 4$ because we are solving the 4 month pig breeding problem in this example. ```julia using JuMP, Gurobi using DecisionProgramming const N = 4 -const health = [3*k - 2 for k in 1:N] -const test = [3*k - 1 for k in 1:(N-1)] -const treat = [3*k for k in 1:(N-1)] -const cost = [(3*N - 2) + k for k in 1:(N-1)] -const price = [(3*N - 2) + N] -const health_states = ["ill", "healthy"] -const test_states = ["positive", "negative"] -const treat_states = ["treat", "pass"] - -S = States([ - (length(health_states), health), - (length(test_states), test), - (length(treat_states), treat), -]) -C = Vector{ChanceNode}() -D = Vector{DecisionNode}() -V = Vector{ValueNode}() -X = Vector{Probabilities}() -Y = Vector{Consequences}() + +diagram = InfluenceDiagram() ``` -Next, we define the nodes with their information sets and corresponding probabilities or consequences. +Next, we define the nodes with their information sets and states. We add the nodes to the influence diagram. ### Health at First Month -As seen in the influence diagram, the node $h_1$ has no arcs into it, making it a root node. Therefore, the information set $I(h_1)$ is empty. +As seen in the influence diagram, the node $h_1$ has no arcs into it, making it a root node. Therefore, the information set $I(h_1)$ is empty. The state of this node are $ill$ and $healthy$. + + +```julia +add_node!(diagram, ChanceNode("H1", [], ["ill", "healthy"])) +``` + +### Health, Test Results and Treatment Decisions at Subsequent Months +The chance and decision nodes representing the health, test results, treatment decisions for the following months can be added easily using a for-loop. The value node representing the testing costs in each month is also added. Each node is given a name, its information set and states. Notice, that value nodes do not have states because their purpose is to map their information state to utilities, which will be added later. Notice also, that here we do not assume the no-forgetting principle and thus the information set of the treatment decision is made only based on the previous test result. Remember, that the first health node $h_1$ was already added above. + +```julia +for i in 1:N-1 + # Testing result + add_node!(diagram, ChanceNode("T$i", ["H$i"], ["positive", "negative"])) + # Decision to treat + add_node!(diagram, DecisionNode("D$i", ["T$i"], ["treat", "pass"])) + # Cost of treatment + add_node!(diagram, ValueNode("C$i", ["D$i"])) + # Health of next period + add_node!(diagram, ChanceNode("H$(i+1)", ["H$(i)", "D$(i)"], ["ill", "healthy"])) +end +``` + +### Market Price +The final value node represented the market price is added. It has the final health node $h_n$ as its information set. +```julia +add_node!(diagram, ValueNode("MP", ["H$N"])) +``` + +### Generate arcs +Now that all of the nodes have been added to our influence diagram we generate the arcs between the nodes. This step automatically orders the nodes, gives them indices and reorganises the information into the appropriate form. +```julia +generate_arcs!(diagram) +``` + +### Probabilities -The probability that pig is ill in the first month is +We define probability distributions for all chance nodes. For the first health node, the probability distribution is defined over its two states $ill$ and $healthy$. The probability that pig is ill in the first month is $$ℙ(h_1 = ill)=0.1.$$ @@ -59,53 +77,31 @@ We obtain the complement probabilities for binary states by subtracting from one $$ℙ(h_1 = healthy)=1-ℙ(h_1 = ill).$$ -In decision programming, we add the nodes and probabilities as follows: - +In decision programming, we add these probabilities for node $h_1$ as follows. Notice, that the probability vector is ordered according to the order of states when defining node $h_1$. ```julia -for j in health[[1]] - I_j = Vector{Node}() - X_j = zeros(S[I_j]..., S[j]) - X_j[1] = 0.1 - X_j[2] = 1.0 - X_j[1] - push!(C, ChanceNode(j, I_j)) - push!(X, Probabilities(j, X_j)) -end +add_probabilities!(diagram, "H1", [0.1, 0.9]) ``` -### Health at Subsequent Months -The probability that the pig is ill in the subsequent months $k=2,...,N$ depends on the treatment decision and state of health in the previous month $k-1$. The nodes $h_{k-1}$ and $d_{k-1}$ are thus in the information set $I(h_k)$, meaning that the probability distribution of $h_k$ is conditional on these nodes: +The probability distributions for the other health nodes are identical, thus we define one probability matrix and use it for all the subsequent months' health nodes. The probability that the pig is ill in the subsequent months $k=2,...,N$ depends on the treatment decision and state of health in the previous month $k-1$. The nodes $h_{k-1}$ and $d_{k-1}$ are thus in the information set $I(h_k)$, meaning that the probability distribution of $h_k$ is conditional on these nodes: -$$ℙ(h_k = ill ∣ d_{k-1} = pass, h_{k-1} = healthy)=0.2,$$ +$$ℙ(h_k = ill ∣ h_{k-1} = healthy, \ d_{k-1} = pass)=0.2,$$ -$$ℙ(h_k = ill ∣ d_{k-1} = treat, h_{k-1} = healthy)=0.1,$$ +$$ℙ(h_k = ill ∣ h_{k-1} = healthy, \ d_{k-1} = treat)=0.1,$$ -$$ℙ(h_k = ill ∣ d_{k-1} = pass, h_{k-1} = ill)=0.9,$$ +$$ℙ(h_k = ill ∣ h_{k-1} = ill, \ d_{k-1} = pass)=0.9,$$ -$$ℙ(h_k = ill ∣ d_{k-1} = treat, h_{k-1} = ill)=0.5.$$ - -In decision programming: +$$ℙ(h_k = ill ∣ h_{k-1} = ill, \ d_{k-1} = treat)=0.5.$$ +The probability matrix is define in Decision Programming in the following way. Notice, that the ordering of the information state corresponds to the order in which the information set was defined when adding the health nodes. ```julia -for (i, k, j) in zip(health[1:end-1], treat, health[2:end]) - I_j = [i, k] - X_j = zeros(S[I_j]..., S[j]) - X_j[2, 2, 1] = 0.2 - X_j[2, 2, 2] = 1.0 - X_j[2, 2, 1] - X_j[2, 1, 1] = 0.1 - X_j[2, 1, 2] = 1.0 - X_j[2, 1, 1] - X_j[1, 2, 1] = 0.9 - X_j[1, 2, 2] = 1.0 - X_j[1, 2, 1] - X_j[1, 1, 1] = 0.5 - X_j[1, 1, 2] = 1.0 - X_j[1, 1, 1] - push!(C, ChanceNode(j, I_j)) - push!(X, Probabilities(j, X_j)) -end +X_H = ProbabilityMatrix(diagram, "H2") +set_probability!(X_H, ["healthy", "pass", :], [0.2, 0.8]) +set_probability!(X_H, ["healthy", "treat", :], [0.1, 0.9]) +set_probability!(X_H, ["ill", "pass", :], [0.9, 0.1]) +set_probability!(X_H, ["ill", "treat", :], [0.5, 0.5]) ``` -Note that the order of states indexing the probabilities is reversed compared to the mathematical definition. - -### Health Test -For the probabilities that the test indicates a pig's health correctly at month $k=1,...,N-1$, we have +Next we define the probability matrix for the test results. Here again, we note that the probability distributions for all test results are identical, and thus we only define the matrix once. For the probabilities that the test indicates a pig's health correctly at month $k=1,...,N-1$, we have $$ℙ(t_k = positive ∣ h_k = ill) = 0.8,$$ @@ -114,52 +110,39 @@ $$ℙ(t_k = negative ∣ h_k = healthy) = 0.9.$$ In decision programming: ```julia -for (i, j) in zip(health, test) - I_j = [i] - X_j = zeros(S[I_j]..., S[j]) - X_j[1, 1] = 0.8 - X_j[1, 2] = 1.0 - X_j[1, 1] - X_j[2, 2] = 0.9 - X_j[2, 1] = 1.0 - X_j[2, 2] - push!(C, ChanceNode(j, I_j)) - push!(X, Probabilities(j, X_j)) -end +X_T = ProbabilityMatrix(diagram, "T1") +set_probability!(X_T, ["ill", "positive"], 0.8) +set_probability!(X_T, ["ill", "negative"], 0.2) +set_probability!(X_T, ["healthy", "negative"], 0.9) +set_probability!(X_T, ["healthy", "positive"], 0.1) ``` -### Decision to Treat -In decision programing, we add the decision nodes for decision to treat the pig as follows: +We add the probability matrices into the influence diagram as follows. ```julia -for (i, j) in zip(test, treat) - I_j = [i] - push!(D, DecisionNode(j, I_j)) +for i in 1:N-1 + add_probabilities!(diagram, "T$i", X_T) + add_probabilities!(diagram, "H$(i+1)", X_H) end ``` -The no-forgetting assumption does not hold, and the information set $I(d_k)$ only comprises the previous test result. -### Cost of Treatment +### Utilities + The cost of treatment decision for the pig at month $k=1,...,N-1$ is defined $$Y(d_k=treat) = -100,$$ $$Y(d_k=pass) = 0.$$ -In decision programming: +In decision programming the utility values are added as follows. Notice that the values in the utility matrix are ordered according to the order in which the information set was given when adding the node. ```julia -for (i, j) in zip(treat, cost) - I_j = [i] - Y_j = zeros(S[I_j]...) - Y_j[1] = -100 - Y_j[2] = 0 - push!(V, ValueNode(j, I_j)) - push!(Y, Consequences(j, Y_j)) +for i in 1:N-1 + add_utilities!(diagram, "C$i", [-100.0, 0.0]) end ``` - -### Selling Price -The price of given the pig health at month $N$ is defined +The market price of given the pig health at month $N$ is defined $$Y(h_N=ill) = 300,$$ @@ -168,50 +151,32 @@ $$Y(h_N=healthy) = 1000.$$ In decision programming: ```julia -for (i, j) in zip(health[end], price) - I_j = [i] - Y_j = zeros(S[I_j]...) - Y_j[1] = 300 - Y_j[2] = 1000 - push!(V, ValueNode(j, I_j)) - push!(Y, Consequences(j, Y_j)) -end +add_utilities!(diagram, "MP", [300.0, 1000.0]) ``` -### Validating Influence Diagram -Finally, we need to validate the influence diagram and sort the nodes, probabilities and consequences in increasing order by the node indices. +### Generate Influence Diagram +Finally, we generate the full influence diagram before defining the decision model. By default this function uses the default path probabilities and utilities, which are defined as the joint probability of all chance events in the diagram and the sum of utilities in value nodes, respectively. In the [Contingent Portfolio Programming](contingent-portfolio-programming.md) example, we show how to use a user-defined custom path utility function. -```julia -validate_influence_diagram(S, C, D, V) -sort!.((C, D, V, X, Y), by = x -> x.j) -``` - -We define the path probability. -```julia -P = DefaultPathProbability(C, X) -``` +In the pig breeding problem, when the $N$ is large some of the path utilities become negative. In this case, we choose to use the [positive path utility](../decision_model.md) transformation, which allows us to exclude the probability cut in the next section. -As the path utility, we use the default, which is the sum of the consequences given the path. ```julia -U = DefaultPathUtility(V, Y) +generate_diagram!(diagram, positive_path_utility = true) ``` - ## Decision Model -We apply an affine transformation to the utility function, making all path utilities positive. Now that all path utilities are positive, the probability cut can be excluded from the model. The purpose of this is discussed in the [theoretical section](../decision-programming/decision-model.md) of this documentation. +Next we initialise the JuMP model and add the decision variables. Then we addd the path compatibility variables. Since we applied an affine transformation to the utility function, making all path utilities positive, the probability cut can be excluded from the model. The purpose of this is discussed in the [theoretical section](../decision-programming/decision-model.md) of this documentation. ```julia -U⁺ = PositivePathUtility(S, U) model = Model() -z = DecisionVariables(model, S, D) -x_s = PathCompatibilityVariables(model, z, S, P, probability_cut = false) +z = DecisionVariables(model, diagram) +x_s = PathCompatibilityVariables(model, diagram, z, probability_cut = false) ``` We create the objective function ```julia -EV = expected_value(model, x_s, U⁺, P) +EV = expected_value(model, diagram, x_s) @objective(model, Max, EV) ``` @@ -228,87 +193,91 @@ optimize!(model) ## Analyzing Results -### Decision Strategy -We obtain the optimal decision strategy: +Once the model is solved, we extract the results. The results are the decision strategy, state probabilities and utility distribution. ```julia Z = DecisionStrategy(z) +S_probabilities = StateProbabilities(diagram, Z) +U_distribution = UtilityDistribution(diagram, Z) ``` +### Decision Strategy + +The optimal decision strategy is: + ```julia-repl -julia> print_decision_strategy(S, Z) -┌────────┬──────┬───┐ -│ Nodes │ (2,) │ 3 │ -├────────┼──────┼───┤ -│ States │ (1,) │ 2 │ -│ States │ (2,) │ 2 │ -└────────┴──────┴───┘ -┌────────┬──────┬───┐ -│ Nodes │ (5,) │ 6 │ -├────────┼──────┼───┤ -│ States │ (1,) │ 1 │ -│ States │ (2,) │ 2 │ -└────────┴──────┴───┘ -┌────────┬──────┬───┐ -│ Nodes │ (8,) │ 9 │ -├────────┼──────┼───┤ -│ States │ (1,) │ 1 │ -│ States │ (2,) │ 2 │ -└────────┴──────┴───┘ +julia> print_decision_strategy(diagram, Z, S_probabilities) +┌────────────────┬────────────────┐ +│ State(s) of T1 │ Decision in D1 │ +├────────────────┼────────────────┤ +│ positive │ pass │ +│ negative │ pass │ +└────────────────┴────────────────┘ +┌────────────────┬────────────────┐ +│ State(s) of T2 │ Decision in D2 │ +├────────────────┼────────────────┤ +│ positive │ treat │ +│ negative │ pass │ +└────────────────┴────────────────┘ +┌────────────────┬────────────────┐ +│ State(s) of T3 │ Decision in D3 │ +├────────────────┼────────────────┤ +│ positive │ treat │ +│ negative │ pass │ +└────────────────┴────────────────┘ ``` -The optimal strategy is as follows. In the first period, state 2 (no treatment) is chosen in node 3 ($d_1$) regardless of the state of node 2 ($t_1$). In other words, the pig is not treated in the first month. In the two subsequent months, state 1 (treat) is chosen if the corresponding test result is 1 (positive). +The optimal strategy is to not treat the pig in the first month regardless of if it is sick or not. In the two subsequent months, the pig should be treated if the test result is positive. ### State Probabilities -The state probabilities for the strategy $Z$ can also be obtained. These tell the probability of each state in each node, given the strategy $Z$. +The state probabilities for the strategy $Z$ tell the probability of each state in each node, given the strategy $Z$. -```julia -sprobs = StateProbabilities(S, P, Z) -``` ```julia-repl -julia> print_state_probabilities(sprobs, health) -┌───────┬──────────┬──────────┬─────────────┐ -│ Node │ State 1 │ State 2 │ Fixed state │ -│ Int64 │ Float64 │ Float64 │ String │ -├───────┼──────────┼──────────┼─────────────┤ -│ 1 │ 0.100000 │ 0.900000 │ │ -│ 4 │ 0.270000 │ 0.730000 │ │ -│ 7 │ 0.295300 │ 0.704700 │ │ -│ 10 │ 0.305167 │ 0.694833 │ │ -└───────┴──────────┴──────────┴─────────────┘ -julia> print_state_probabilities(sprobs, test) -┌───────┬──────────┬──────────┬─────────────┐ -│ Node │ State 1 │ State 2 │ Fixed state │ -│ Int64 │ Float64 │ Float64 │ String │ -├───────┼──────────┼──────────┼─────────────┤ -│ 2 │ 0.170000 │ 0.830000 │ │ -│ 5 │ 0.289000 │ 0.711000 │ │ -│ 8 │ 0.306710 │ 0.693290 │ │ -└───────┴──────────┴──────────┴─────────────┘ -julia> print_state_probabilities(sprobs, treat) -┌───────┬──────────┬──────────┬─────────────┐ -│ Node │ State 1 │ State 2 │ Fixed state │ -│ Int64 │ Float64 │ Float64 │ String │ -├───────┼──────────┼──────────┼─────────────┤ -│ 3 │ 0.000000 │ 1.000000 │ │ -│ 6 │ 0.289000 │ 0.711000 │ │ -│ 9 │ 0.306710 │ 0.693290 │ │ -└───────┴──────────┴──────────┴─────────────┘ +julia> health_nodes = [["H$i" for i in 1:N]...] +julia> print_state_probabilities(diagram, S_probabilities, health_nodes) + +┌────────┬──────────┬──────────┬─────────────┐ +│ Node │ State 1 │ State 2 │ Fixed state │ +│ String │ Float64 │ Float64 │ String │ +├────────┼──────────┼──────────┼─────────────┤ +│ H1 │ 0.100000 │ 0.900000 │ │ +│ H2 │ 0.270000 │ 0.730000 │ │ +│ H3 │ 0.295300 │ 0.704700 │ │ +│ H4 │ 0.305167 │ 0.694833 │ │ +└────────┴──────────┴──────────┴─────────────┘ + +julia> test_nodes = [["T$i" for i in 1:N-1]...] +julia> print_state_probabilities(diagram, S_probabilities, test_nodes) +┌────────┬──────────┬──────────┬─────────────┐ +│ Node │ State 1 │ State 2 │ Fixed state │ +│ String │ Float64 │ Float64 │ String │ +├────────┼──────────┼──────────┼─────────────┤ +│ T1 │ 0.170000 │ 0.830000 │ │ +│ T2 │ 0.289000 │ 0.711000 │ │ +│ T3 │ 0.306710 │ 0.693290 │ │ +└────────┴──────────┴──────────┴─────────────┘ + +julia> treatment_nodes = [["D$i" for i in 1:N-1]...] +julia> print_state_probabilities(diagram, S_probabilities, treatment_nodes) +┌────────┬──────────┬──────────┬─────────────┐ +│ Node │ State 1 │ State 2 │ Fixed state │ +│ String │ Float64 │ Float64 │ String │ +├────────┼──────────┼──────────┼─────────────┤ +│ D1 │ 0.000000 │ 1.000000 │ │ +│ D2 │ 0.289000 │ 0.711000 │ │ +│ D3 │ 0.306710 │ 0.693290 │ │ +└────────┴──────────┴──────────┴─────────────┘ ``` ### Utility Distribution -We can also print the utility distribution for the optimal strategy. The selling prices for a healthy and an ill pig are 1000DKK and 300DKK, respectively, while the cost of treatment is 100DKK. We can see that the probability of the pig being ill in the end is the sum of three first probabilities, approximately 30.5%. This matches the probability of state 1 in node 10 in the state probabilities shown above. - -```julia -udist = UtilityDistribution(S, P, U, Z) -``` +We can also print the utility distribution for the optimal strategy. The selling prices for a healthy and an ill pig are 1000DKK and 300DKK, respectively, while the cost of treatment is 100DKK. We can see that the probability of the pig being ill in the end is the sum of three first probabilities, approximately 30.5%. This matches the probability of state $ill$ in the last node $h_4$ in the state probabilities shown above. ```julia-repl -julia> print_utility_distribution(udist) +julia> print_utility_distribution(U_distribution) ┌─────────────┬─────────────┐ │ Utility │ Probability │ │ Float64 │ Float64 │ @@ -325,7 +294,7 @@ julia> print_utility_distribution(udist) Finally, we print some statistics for the utility distribution. The expected value of the utility is 727DKK, the same as in [^1]. ```julia-repl -julia> print_statistics(udist) +julia> print_statistics(U_distribution) ┌──────────┬────────────┐ │ Name │ Statistics │ │ String │ Float64 │ diff --git a/examples/pig_breeding.jl b/examples/pig_breeding.jl index 1584067f..ca6629c1 100644 --- a/examples/pig_breeding.jl +++ b/examples/pig_breeding.jl @@ -18,7 +18,7 @@ for i in 1:N-1 # Health of next period add_node!(diagram, ChanceNode("H$(i+1)", ["H$(i)", "D$(i)"], ["ill", "healthy"])) end -add_node!(diagram, ValueNode("SP", ["H$N"])) +add_node!(diagram, ValueNode("MP", ["H$N"])) generate_arcs!(diagram) @@ -41,11 +41,14 @@ set_probability!(X_T, ["healthy", "positive"], 0.1) for i in 1:N-1 add_probabilities!(diagram, "T$i", X_T) - add_utilities!(diagram, "C$i", [-100.0, 0.0]) add_probabilities!(diagram, "H$(i+1)", X_H) end -add_utilities!(diagram, "SP", [300.0, 1000.0]) +for i in 1:N-1 + add_utilities!(diagram, "C$i", [-100.0, 0.0]) +end + +add_utilities!(diagram, "MP", [300.0, 1000.0]) generate_diagram!(diagram, positive_path_utility = true) From 06faa15ed757a11051d07cca89432322dc199907 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Mon, 30 Aug 2021 13:20:39 +0300 Subject: [PATCH 082/133] Corrections to used car buyer example documentation. --- docs/src/examples/used-car-buyer.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/src/examples/used-car-buyer.md b/docs/src/examples/used-car-buyer.md index 22171663..65833621 100644 --- a/docs/src/examples/used-car-buyer.md +++ b/docs/src/examples/used-car-buyer.md @@ -63,7 +63,7 @@ add_node!(diagram, ValueNode("V3", ["O", "A"])) ``` ### Generate arcs -Now that all of the nodes have been added to our influence diagram we must generate the arcs. This step orders the nodes, gives them indices and reorganises the information into the appropriate form. +Now that all of the nodes have been added to our influence diagram we generate the arcs between the nodes. This step automatically orders the nodes, gives them indices and reorganises the information into the appropriate form. ```julia generate_arcs!(diagram) ``` @@ -90,7 +90,7 @@ add_probabilities!(diagram, "R", X_R) ``` -### Testing Cost +### Utilities We continue by defining the utilities associated with value nodes. The utilities $Y_j(𝐬_{I(j)})$ are defined and added similarly to the probabilities. Value node $V1$ has only node $T$ in its information set and node $T$ only has two states. Therefore, node $V1$ needs to map exactly two utility values, on for state $tes$ and the other for $no test$. @@ -102,7 +102,7 @@ set_utility!(Y_V1, ["no test"], 0) add_utilities!(diagram, "V1", Y_V1) ``` -### Base Profit of Purchase +We then define the utilities describing the base profit of of the purchase. ```julia Y_V2 = UtilityMatrix(diagram, "V2") set_utility!(Y_V2, ["buy without guarantee"], 100) @@ -111,8 +111,7 @@ set_utility!(Y_V2, ["don't buy"], 0) add_utilities!(diagram, "V2", Y_V2) ``` -### Repair Cost -The rows of the utilities matrix `Y_V3` correspond to the state of the car, while the columns correspond to the decision made in node $A$. The utilities can be added as follows. Notice that the utility values for the second row are added in one line, in this case it is important to give the utility values in the right order. The order of the columns is determined by the order in which the states are given when declaring node $A$. See the [usage page](../usage.md) for more on this more compact syntax. +Finally, we add the utilities corresponding to the repair costs. The rows of the utilities matrix `Y_V3` correspond to the state of the car, while the columns correspond to the decision made in node $A$. The utilities can be added as follows. Notice that the utility values for the second row are added in one line, in this case it is important to give the utility values in the right order. The order of the columns is determined by the order in which the states are given when declaring node $A$. See the [usage page](../usage.md) for more on this more compact syntax. ```julia Y_V3 = UtilityMatrix(diagram, "V3") set_utility!(Y_V3, ["lemon", "buy without guarantee"], -200) @@ -153,7 +152,6 @@ optimize!(model) ## Analyzing Results -### Decision Strategy Once the model is solved, we extract the results. The results are the decision strategy, state probabilities and utility distribution. ```julia @@ -162,9 +160,11 @@ S_probabilities = StateProbabilities(diagram, Z) U_distribution = UtilityDistribution(diagram, Z) ``` +### Decision Strategy + We obtain the following optimal decision strategy: ```julia-repl -julia> print_decision_strategy(S, Z) +julia> print_decision_strategy(diagram, Z, S_probabilities) ┌───────────────┐ │ Decision in T │ ├───────────────┤ @@ -193,7 +193,7 @@ julia> print_utility_distribution(U_distribution) From the utility distribution, we can see that Joe's profit with this strategy is 15 USD, with a 20% probability (the car is a lemon) and 35 USD with an 80% probability (the car is a peach). ```julia-repl -julia> print_statistics(udist) +julia> print_statistics(U_distribution) ┌──────────┬────────────┐ │ Name │ Statistics │ │ String │ Float64 │ From a2b3cb40335a1fdaf70affd50de633bae8d8b7bb Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Mon, 30 Aug 2021 13:49:15 +0300 Subject: [PATCH 083/133] Fixed two bugs in analysis.jl. One in compatible paths enumeration and second in StateProbabilities. --- src/analysis.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/analysis.jl b/src/analysis.jl index 0ac2b431..cd39afa9 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -57,7 +57,7 @@ function Base.iterate(S_Z::CompatiblePaths) iter = paths(S_Z.S[S_Z.C]) else ks = sort(collect(keys(S_Z.fixed))) - fixed = Dict{Node, State}(i => S_Z.fixed[k] for (i, k) in enumerate(ks)) + fixed = Dict{Node, State}(Node(i) => S_Z.fixed[k] for (i, k) in enumerate(S_Z.C) if k in ks) iter = paths(S_Z.S[S_Z.C], fixed) end next = iterate(iter) @@ -174,7 +174,8 @@ function StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy, node state_index = findfirst(j -> j == state, diagram.States[node_index]) prior = prior_probabilities.probs[node_index][state_index] - fixed = prior_probabilities.fixed + fixed = deepcopy(prior_probabilities.fixed) + push!(fixed, node_index => state_index) probs = Dict(i => zeros(diagram.S[i]) for i in 1:length(diagram.S)) for s in CompatiblePaths(diagram, Z, fixed), i in 1:length(diagram.S) From 52a7aa4459ae2a08466a18f1b26a5990ae706aaf Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 31 Aug 2021 09:49:26 +0300 Subject: [PATCH 084/133] Fixed couple mistakes in docstrings. --- src/influence_diagram.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index aef9eee2..722dad34 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -490,7 +490,7 @@ Base.setindex!(PM::ProbabilityMatrix{N}, X, I::Vararg{Any, N}) where N = (PM.mat """ function ProbabilityMatrix(diagram::InfluenceDiagram, node::Name) -Initialise a probability matrix for a given chance node. +Initialise a probability matrix for a given chance node. The matrix is initialised with zeros. # Examples ```julia @@ -632,7 +632,7 @@ Base.setindex!(UM::UtilityMatrix{N}, Y, I::Vararg{Any, N}) where N = (UM.matrix[ """ function UtilityMatrix(diagram::InfluenceDiagram, node::Name) -Initialise a utility matrix for a value node. +Initialise a utility matrix for a value node. The matrix is initialised with `Inf` values. # Examples ```julia @@ -979,7 +979,7 @@ function ForbiddenPath(diagram::InfluenceDiagram, nodes::Vector{Name}, paths::Ve end """ - function ForbiddenPath(diagram::InfluenceDiagram, nodes::Vector{Name}, paths::Vector{NTuple{N, Name}}) where N + function FixedPath(diagram::InfluenceDiagram, fixed::Dict{Name, Name}) FixedPath outer construction function. Create FixedPath variable. From 7d694ff212a749f959e2428554312735e92f4130 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 31 Aug 2021 09:52:42 +0300 Subject: [PATCH 085/133] Updated CHD example and its dokumentation to new interface. --- docs/src/examples/CHD_preventative_care.md | 336 ++++++++++----------- examples/CHD_preventative_care.jl | 199 ++++-------- 2 files changed, 220 insertions(+), 315 deletions(-) diff --git a/docs/src/examples/CHD_preventative_care.md b/docs/src/examples/CHD_preventative_care.md index e6d0660a..cd661c27 100644 --- a/docs/src/examples/CHD_preventative_care.md +++ b/docs/src/examples/CHD_preventative_care.md @@ -18,7 +18,7 @@ The prior risk estimate represented by node $R0$ influences the health node $H$, The value nodes in the model are $TC$ and $HB$. Node $TC$ represents the testing costs incurred due to the testing decisions $T1$ and $T2$. Node $HB$ represents the health benefits achieved. The testing costs and health benefits are measured in QALYs. These parameter values were evaluated in the study [^2]. -We begin by declaring the chosen prior risk level and reading the conditional probability data for the tests. The risk level 12% is referred to as 13 because indexing begins from 1 and the first risk level is 0\%. Note also that the sample data in this repository is dummy data due to distribution restrictions on the real data. We also define functions ```update_risk_distribution ```, ```state_probabilities``` and ```analysing_results ```. These functions will be discussed in the following sections. +We begin by declaring the chosen prior risk level and reading the conditional probability data for the tests. Note that the sample data in this repository is dummy data due to distribution restrictions on the real data. We also define functions ```update_risk_distribution ``` and ```state_probabilities```. These functions will be discussed in the following sections. ```julia using Logging @@ -27,7 +27,7 @@ using DecisionProgramming using CSV, DataFrames, PrettyTables -const chosen_risk_level = 13 +const chosen_risk_level = 12% const data = CSV.read("risk_prediction_data.csv", DataFrame) function update_risk_distribution(prior::Int64, t::Int64)... @@ -35,52 +35,51 @@ end function state_probabilities(risk_p::Array{Float64}, t::Int64, h::Int64, prior::Int64)... end - -function analysing_results(Z::DecisionStrategy, sprobs::StateProbabilities)... -end ``` - -We define the decision programming model by first defining the node indices and states: +### Initialise influence diagram +We start defining the decision programming model by initialising the influence diagram. ```julia -const R0 = 1 -const H = 2 -const T1 = 3 -const R1 = 4 -const T2 = 5 -const R2 = 6 -const TD = 7 -const TC = 8 -const HB = 9 +diagram = InfluenceDiagram() +``` + +For brevity in the following, we define the states of the nodes to be readily available. Notice, that $R_{states}$ is a vector with values $0\%, 1\%,..., 100\%$. +```julia const H_states = ["CHD", "no CHD"] const T_states = ["TRS", "GRS", "no test"] const TD_states = ["treatment", "no treatment"] -const R_states = map( x -> string(x) * "%", [0:1:100;]) -const TC_states = ["TRS", "GRS", "TRS & GRS", "no tests"] -const HB_states = ["CHD & treatment", "CHD & no treatment", "no CHD & treatment", "no CHD & no treatment"] - -@info("Creating the influence diagram.") -S = States([ - (length(R_states), [R0, R1, R2]), - (length(H_states), [H]), - (length(T_states), [T1, T2]), - (length(TD_states), [TD]) -]) - -C = Vector{ChanceNode}() -D = Vector{DecisionNode}() -V = Vector{ValueNode}() -X = Vector{Probabilities}() -Y = Vector{Consequences}() +const R_states = [string(x) * "%" for x in [0:1:100;]] ``` + + Then we add the nodes. The chance and decision nodes are identified by their names. When declaring the nodes, they are also given information sets and states. Notice that nodes $R0$ and $H$ are root nodes, meaning that their information sets are empty. In Decision Programming, we add the chance and decision nodes in the follwoing way. + ```julia +add_node!(diagram, ChanceNode("R0", [], R_states)) +add_node!(diagram, ChanceNode("R1", ["R0", "H", "T1"], R_states)) +add_node!(diagram, ChanceNode("R2", ["R1", "H", "T2"], R_states)) +add_node!(diagram, ChanceNode("H", ["R0"], H_states)) + +add_node!(diagram, DecisionNode("T1", ["R0"], T_states)) +add_node!(diagram, DecisionNode("T2", ["R1"], T_states)) +add_node!(diagram, DecisionNode("TD", ["R2"], TD_states)) +``` + +The value nodes are added in a similar fashion. However, value nodes do not have states because their purpose is to map their information states to utility values. Thus, value nodes are only given a name and their information set. +```julia +add_node!(diagram, ValueNode("TC", ["T1", "T2"])) +add_node!(diagram, ValueNode("HB", ["H", "TD"])) +``` -Next, we define the nodes with their information sets and corresponding probabilities for chance nodes and consequences for value nodes. +### Generate arcs +Now that all of the nodes have been added to the influence diagram we generate the arcs between the nodes. This step automatically orders the nodes, gives them indices and reorganises the information into the appropriate form. +```julia +generate_arcs!(diagram) +``` -### Prior risk estimate and health of the patient +### Probabilities of the prior risk estimate and health of the patient In this subproblem, the prior risk estimate is given and therefore the node $R0$ is in effect a deterministic node. In decision programming a deterministic node is added as a chance node for which the probability of one state is set to one and the probabilities of the rest of the states are set to zero. In this case @@ -88,16 +87,14 @@ $$ℙ(R0 = 12\%)=1$$ and $$ℙ(R0 \neq 12\%)= 0. $$ -Notice also that node $R0$ is the first node in the influence diagram, meaning that its information set $I(R0)$ is empty. In decision programming we add node $R0$ and its state probabilities as follows: +The probability matrix of node $R0$ is added in the following way. Remember that the `ProbabilityMatrix` function initialises the matrix with zeros. ```julia -I_R0 = Vector{Node}() -X_R0 = zeros(S[R0]) -X_R0[chosen_risk_level] = 1 -push!(C, ChanceNode(R0, I_R0)) -push!(X, Probabilities(R0, X_R0)) +X_R0 = ProbabilityMatrix(diagram, "R0") +set_probability!(X_R0, [chosen_risk_level], 1) +add_probabilities!(diagram, "R0", X_R0) ``` -Next we add node $H$ and its state probabilities. For modeling purposes, we define the information set of node $H$ to include the prior risk node $R0$. We set the probability that the patient experiences a CHD event in the next ten years according to the prior risk level such that +Next we add the state probabilities of node $H$. For modeling purposes, we define the information set of node $H$ to include the prior risk node $R0$. We set the probability that the patient experiences a CHD event in the next ten years according to the prior risk level such that $$ℙ(H = \text{CHD} | R0 = \alpha) = \alpha.$$ @@ -107,25 +104,15 @@ $$ℙ(H = \text{no CHD} | R0 = \alpha) = 1 - \alpha$$ Since node $R0$ is deterministic and the health node $H$ is defined in this way, in our model the patient has a 12% chance of experiencing a CHD event and 88% chance of remaining healthy. -Node $H$ and its probabilities are added in the following way. - +In Decision Programming the probability matrix of node $H$ has dimensions (101, 2) because its information set consisting of node $R0$ has 101 states and node $H$ has 2 states. We first set the column related to the state $CHD$ with values from `data.risk_levels` which are $0.00, 0.01, ..., 0.99, 1.00$ and the other column as it s complement event. ```julia -I_H = [R0] -X_H = zeros(S[R0], S[H]) -X_H[:, 1] = data.risk_levels -X_H[:, 2] = 1 .- X_H[:, 1] -push!(C, ChanceNode(H, I_H)) -push!(X, Probabilities(H, X_H)) +X_H = ProbabilityMatrix(diagram, "H") +set_probability!(X_H, [:, "CHD"], data.risk_levels) +set_probability!(X_H, [:, "no CHD"], 1 .- data.risk_levels) +add_probabilities!(diagram, "H", X_H) ``` -### Test decisions and updating the risk estimate - -The node representing the first test decision is added to the model. - -```julia -I_T1 = [R0] -push!(D, DecisionNode(T1, I_T1)) -``` +### Probabilities of the updated the risk estimates For node $R1%$, the probabilities of the states are calculated by aggregating the updated risk estimates, after a test is performed, into the risk levels. The updated risk estimates are calculated using the function ```update_risk_distribution```, which calculates the posterior probability distribution for a given health state, test and prior risk estimate. @@ -133,114 +120,94 @@ $$\textit{risk estimate} = P(\text{CHD} \mid \text{test result}) = \frac{P(\text The probabilities $P(\text{test result} \mid \text{CHD})$ are test specific and these are read from the CSV data file. The updated risk estimates are aggregated according to the risk levels. These aggregated probabilities are then the state probabilities of node $R1$. The aggregating is done using function ```state_probabilities```. -The node $R1$ and its probabilities are added in the following way. +In Decision Programming the probability distribution over the states of node $R1$ is defined into a $(101,2,3,101)$ probability matrix. This is because its information set consists of ($R0, H, T$) which have 101, 2 and 3 states respectively and the node $R1$ itself has 101 states. Here, one must know that in Decision Programming the states of the nodes are mapped to indices in the back-end. For instance, the health states $\text{CHD}$ and $\text{no CHD}$ are indexed 1 and 2. The testing decision states TRS,n GRS and no test are 1, 2 and 3. The order of the states is determined by the order in which they are defined when adding the nodes. Knowing this, we can declare the probability values straight into the probability matrix using a very compact syntax. Notice that we add 101 probability values at a time into the matrix. ```julia -I_R1 = [R0, H, T1] -X_R1 = zeros(S[I_R1]..., S[R1]) +X_R = ProbabilityMatrix(diagram, "R1") for s_R0 = 1:101, s_H = 1:2, s_T1 = 1:3 - X_R1[s_R0, s_H, s_T1, :] = state_probabilities(update_risk_distribution(s_R0, s_T1), s_T1, s_H, s_R0) + X_R[s_R0, s_H, s_T1, :] = state_probabilities(update_risk_distribution(s_R0, s_T1), s_T1, s_H, s_R0) end -push!(C, ChanceNode(R1, I_R1)) -push!(X, Probabilities(R1, X_R1)) +add_probabilities!(diagram, "R1", X_R) ``` -Nodes $T2$ and $R2$ are added in a similar fashion to nodes $T1$ and $R1$ above. +We notice that the probability distrubtion is identical in $R1$ and $R2$ because their information states are identical. Therefore we can simply add the same matrix from above as the probability matrix of node $R2$. ```julia -I_T2 = [R1] -push!(D, DecisionNode(T2, I_T2)) - - -I_R2 = [H, R1, T2] -X_R2 = zeros(S[I_R2]..., S[R2]) -for s_R1 = 1:101, s_H = 1:2, s_T2 = 1:3 - X_R2[s_H, s_R1, s_T2, :] = state_probabilities(update_risk_distribution(s_R1, s_T2), s_T2, s_H, s_R1) -end -push!(C, ChanceNode(R2, I_R2)) -push!(X, Probabilities(R2, X_R2)) +add_probabilities!(diagram, "R2", X_R) ``` -We also add the treatment decision node $TD$. The treatment decision is made based on the risk estimate achieved with the testing process. - -```julia -I_TD = [R2] -push!(D, DecisionNode(TD, I_TD)) -``` +### Utilities of test costs and health benefits -### Test costs and health benefits - -To add the value node $TC$, which represents testing costs, we need to define the consequences for its different information states. The node and the consequences are added in the following way. +We define a utility matrix for node $TC$, which maps all its information states to testing +costs. The unit in which the testing costs are added is quality-adjusted life-year (QALYs). The utility matrix is defined and added in the following way. ```julia -I_TC = [T1, T2] -Y_TC = zeros(S[I_TC]...) cost_TRS = -0.0034645 cost_GRS = -0.004 -cost_forbidden = 0 #the cost of forbidden test combinations is negligible -Y_TC[1 , 1] = cost_forbidden -Y_TC[1 , 2] = cost_TRS + cost_GRS -Y_TC[1, 3] = cost_TRS -Y_TC[2, 1] = cost_GRS + cost_TRS -Y_TC[2, 2] = cost_forbidden -Y_TC[2, 3] = cost_GRS -Y_TC[3, 1] = cost_TRS -Y_TC[3, 2] = cost_GRS -Y_TC[3, 3] = 0 -push!(V, ValueNode(TC, I_TC)) -push!(Y, Consequences(TC, Y_TC)) +forbidden = 0 #the cost of forbidden test combinations is negligible + +Y_TC = UtilityMatrix(diagram, "TC") +set_utility!(Y_TC, ["TRS", "TRS"], forbidden) +set_utility!(Y_TC, ["TRS", "GRS"], cost_TRS + cost_GRS) +set_utility!(Y_TC, ["TRS", "no test"], cost_TRS) +set_utility!(Y_TC, ["GRS", "TRS"], cost_TRS + cost_GRS) +set_utility!(Y_TC, ["GRS", "GRS"], forbidden) +set_utility!(Y_TC, ["GRS", "no test"], cost_GRS) +set_utility!(Y_TC, ["no test", "TRS"], cost_TRS) +set_utility!(Y_TC, ["no test", "GRS"], cost_GRS) +set_utility!(Y_TC, ["no test", "no test"], 0) +add_utilities!(diagram, "TC", Y_TC) + ``` -The health benefits that are achieved are determined by whether treatment is administered and by the health of the patient. We add the final node to the model. +The health benefits that are achieved are determined by whether treatment is administered and by the health of the patient. We add the final utility matrix to the model. ```julia -I_HB = [H, TD] -Y_HB = zeros(S[I_HB]...) -Y_HB[1 , 1] = 6.89713671259061 # sick & treat -Y_HB[1 , 2] = 6.65436854256236 # sick & don't treat -Y_HB[2, 1] = 7.64528451705134 # healthy & treat -Y_HB[2, 2] = 7.70088349200034 # healthy & don't treat -push!(V, ValueNode(HB, I_HB)) -push!(Y, Consequences(HB, Y_HB)) +Y_HB = UtilityMatrix(diagram, "HB") +set_utility!(Y_HB, ["CHD", "treatment"], 6.89713671259061) +set_utility!(Y_HB, ["CHD", "no treatment"], 6.65436854256236 ) +set_utility!(Y_HB, ["no CHD", "treatment"], 7.64528451705134) +set_utility!(Y_HB, ["no CHD", "no treatment"], 7.70088349200034) +add_utilities!(diagram, "HB", Y_HB) ``` -### Validating the Influence Diagram -Before creating the decision model, we need to validate the influence diagram and sort the nodes, probabilities and consequences in increasing order by the node indices. +### Generate Influence Diagram +Finally, we generate the full influence diagram before defining the decision model. By default this function uses the default path probabilities and utilities, which are defined as the joint probability of all chance events in the diagram and the sum of utilities in value nodes, respectively. In the [Contingent Portfolio Programming](contingent-portfolio-programming.md) example, we show how to use a user-defined custom path utility function. + + +## Decision Model +We define our JuMP model and declare the decision variables. ```julia -validate_influence_diagram(S, C, D, V) -sort!.((C, D, V, X, Y), by = x -> x.j) +model = Model() +z = DecisionVariables(model, diagram) ``` -We also define the path probability and the path utility. We use the default path utility, which is the sum of the consequences of the path. +In this problem, we want to forbid the model from choosing paths where the same test is repeated twice and where the first testing decision is not to perform a test but the second testing decision is to perform a test. We forbid the paths by declaring these combinations of states as forbidden paths. + ```julia -P = DefaultPathProbability(C, X) -U = DefaultPathUtility(V, Y) +forbidden_tests = ForbiddenPath(diagram, ["T1","T2"], [("TRS", "TRS"),("GRS", "GRS"),("no test", "TRS"), ("no test", "GRS")]) ``` +We fix the state of the deterministic $R0$ node by declaring it as a fixed path. Fixing the state of node $R0$ is not necessary because of how the probabilities were defined. However, the fixed state reduces the need for some computation in the back-end. -## Decision Model -We define our model and declare the decision variables. ```julia -model = Model() -z = DecisionVariables(model, S, D) +fixed_R0 = FixedPath(diagram, Dict("R0" => chosen_risk_level)) ``` -In this problem, we want to forbid the model from choosing paths where the same test is repeated twice and where the first testing decision is not to perform a test but the second testing decision is to perform a test. We forbid the paths by declaring these combinations of states as forbidden paths. - -We also choose a scale factor of 1000, which will be used to scale the path probabilities. The probabilities need to be scaled because in this specific problem they are very small since the $R$ nodes have many states. Scaling the probabilities helps the solver find an optimal solution. +We also choose a scale factor of 10000, which will be used to scale the path probabilities. The probabilities need to be scaled because in this specific problem they are very small since the $R$ nodes have a large number of states. Scaling the probabilities helps the solver find an optimal solution. -We declare the path compatibility variables. We fix the state of the deterministic $R0$ node and forbid the unwanted testing strategies and scale the probabilities by giving them as parameters in the function call. +We declare the path compatibility variables. We fix the state of the deterministic $R0$ node , forbid the unwanted testing strategies and scale the probabilities by giving them as parameters in the function call. ```julia -forbidden_tests = ForbiddenPath[([T1,T2], Set([(1,1),(2,2),(3,1), (3,2)]))] scale_factor = 10000.0 -x_s = PathCompatibilityVariables(model, z, S, P; fixed = Dict(1 => chosen_risk_level), forbidden_paths = forbidden_tests, probability_cut=false) +x_s = PathCompatibilityVariables(model, diagram, z; fixed = fixed_R0, forbidden_paths = [forbidden_tests], probability_cut=false) + ``` We define the objective function as the expected value. ```julia -EV = expected_value(model, x_s, U, P, probability_scale_factor= scale_factor) +EV = expected_value(model, diagram, x_s, probability_scale_factor = scale_factor) @objective(model, Max, EV) ``` @@ -259,58 +226,75 @@ optimize!(model) ## Analyzing Results - -### Decision Strategy -We obtain the optimal decision strategy from the z variable values. +We obtain the results in the following way. ```julia Z = DecisionStrategy(z) +S_probabilities = StateProbabilities(diagram, Z) +U_distribution = UtilityDistribution(diagram, Z) + ``` -We use the function ```analysing_results``` to visualise the results in order to inspect the decision strategy. We use this tailor made function merely for convinience. From the printout, we can see that when the prior risk level is 12% the optimal decision strategy is to first perform TRS testing. At the second decision stage, GRS should be conducted if the updated risk estimate is between 16% and 28% and otherwise no further testing should be conducted. Treatment should be provided to those who have a final risk estimate greater than 18%. Notice that the blank spaces in the table are states which have a probability of zero, which means that given this data it is impossible for the patient to have their risk estimate updated to those risk levels. +### Decision Strategy +We inspect the decision strategy. From the printout, we can see that when the prior risk level is 12% the optimal decision strategy is to first perform TRS testing. At the second decision stage, GRS should be conducted if the updated risk estimate is between 16% and 28% and otherwise no further testing should be conducted. Treatment should be provided to those who have a final risk estimate greater than 18%. Notice that the blank spaces in the table are states which have a probability of zero, which means that given this data it is impossible for the patient to have their risk estimate updated to those risk levels. ```julia -sprobs = StateProbabilities(S, P, Z) -``` -```julia -julia> println(analysing_results(Z, sprobs)) -┌─────────────────┬────────┬────────┬────────┐ -│ Information_set │ T1 │ T2 │ TD │ -│ String │ String │ String │ String │ -├─────────────────┼────────┼────────┼────────┤ -│ 0% │ │ 3 │ 2 │ -│ 1% │ │ 3 │ 2 │ -│ 2% │ │ │ 2 │ -│ 3% │ │ 3 │ 2 │ -│ 4% │ │ │ │ -│ 5% │ │ │ │ -│ 6% │ │ 3 │ 2 │ -│ 7% │ │ 3 │ 2 │ -│ 8% │ │ │ 2 │ -│ 9% │ │ │ 2 │ -│ 10% │ │ 3 │ 2 │ -│ 11% │ │ 3 │ 2 │ -│ 12% │ 1 │ │ 2 │ -│ 13% │ │ 3 │ 2 │ -│ 14% │ │ 3 │ 2 │ -│ 15% │ │ │ 2 │ -│ 16% │ │ 2 │ 2 │ -│ 17% │ │ 2 │ 2 │ -│ 18% │ │ 2 │ 1 │ -│ 19% │ │ │ 1 │ -│ 20% │ │ │ 1 │ -│ 21% │ │ 2 │ 1 │ -│ 22% │ │ 2 │ 1 │ -│ 23% │ │ 2 │ 1 │ -│ 24% │ │ │ 1 │ -│ 25% │ │ │ 1 │ -│ 26% │ │ │ 1 │ -│ 27% │ │ │ 1 │ -│ 28% │ │ 3 │ 1 │ -│ 29% │ │ 3 │ 1 │ -│ 30% │ │ │ 1 │ -│ ⋮ │ ⋮ │ ⋮ │ ⋮ │ -└─────────────────┴────────┴────────┴────────┘ - 70 rows omitted +julia> print_decision_strategy(diagram, Z, S_probabilities) +┌────────────────┬────────────────┐ +│ State(s) of R0 │ Decision in T1 │ +├────────────────┼────────────────┤ +│ 12% │ TRS │ +└────────────────┴────────────────┘ +┌────────────────┬────────────────┐ +│ State(s) of R1 │ Decision in T2 │ +├────────────────┼────────────────┤ +│ 0% │ no test │ +│ 1% │ no test │ +│ 3% │ no test │ +│ 6% │ no test │ +│ 7% │ no test │ +│ 10% │ no test │ +│ 11% │ no test │ +│ 13% │ no test │ +│ 14% │ no test │ +│ 16% │ GRS │ +│ 17% │ GRS │ +│ 18% │ GRS │ +│ 21% │ GRS │ +│ 22% │ GRS │ +│ 23% │ GRS │ +│ 28% │ no test │ +│ 29% │ no test │ +│ 31% │ no test │ +│ 34% │ no test │ +│ ⋮ │ ⋮ │ +└────────────────┴────────────────┘ + rows omitted + +┌────────────────┬────────────────┐ +│ State(s) of R2 │ Decision in TD │ +├────────────────┼────────────────┤ +│ 0% │ no treatment │ +│ 1% │ no treatment │ +│ 2% │ no treatment │ +│ 3% │ no treatment │ +│ 6% │ no treatment │ +│ 7% │ no treatment │ +│ 8% │ no treatment │ +│ 9% │ no treatment │ +│ 10% │ no treatment │ +│ 11% │ no treatment │ +│ 12% │ no treatment │ +│ 13% │ no treatment │ +│ 14% │ no treatment │ +│ 15% │ no treatment │ +│ 16% │ no treatment │ +│ 17% │ no treatment │ +│ 18% │ treatment │ +│ 19% │ treatment │ +│ 20% │ treatment │ +│ ⋮ │ ⋮ │ +└────────────────┴────────────────┘ + rows omitted ``` @@ -319,11 +303,7 @@ julia> println(analysing_results(Z, sprobs)) We can also print the utility distribution for the optimal strategy and some basic statistics for the distribution. ```julia -udist = UtilityDistribution(S, P, U, Z) -``` - -```julia -julia> print_utility_distribution(udist) +julia> print_utility_distribution(U_distribution) ┌──────────┬─────────────┐ │ Utility │ Probability │ │ Float64 │ Float64 │ @@ -339,7 +319,7 @@ julia> print_utility_distribution(udist) └──────────┴─────────────┘ ``` ```julia -julia> print_statistics(udist) +julia> print_statistics(U_distribution) ┌──────────┬────────────┐ │ Name │ Statistics │ │ String │ Float64 │ diff --git a/examples/CHD_preventative_care.jl b/examples/CHD_preventative_care.jl index d1c34561..24870234 100644 --- a/examples/CHD_preventative_care.jl +++ b/examples/CHD_preventative_care.jl @@ -6,12 +6,11 @@ using CSV, DataFrames, PrettyTables # Setting subproblem specific parameters -const chosen_risk_level = 13 +const chosen_risk_level = "12%" # Reading tests' technical performance data (dummy data in this case) -data = CSV.read("CHD_preventative_care_data.csv", DataFrame) - +data = CSV.read("examples/CHD_preventative_care_data.csv", DataFrame) # Bayes posterior risk probabilities calculation function @@ -73,13 +72,13 @@ function state_probabilities(risk_p::Array{Float64}, t::Int64, h::Int64, prior:: #if no test is performed, then the probabilities of moving to states (other than the prior risk level) are 0 and to the prior risk element is 1 if t == 3 - state_probabilites = zeros(101, 1) + state_probabilites = zeros(101) state_probabilites[prior] = 1.0 return state_probabilites end # return vector - state_probabilites = zeros(101,1) + state_probabilites = zeros(101) # copying the probabilities of the scores for ease of readability if h == 1 && t == 1 # CHD and TRS @@ -108,157 +107,84 @@ function state_probabilities(risk_p::Array{Float64}, t::Int64, h::Int64, prior:: end -function analysing_results(Z::DecisionStrategy, sprobs::StateProbabilities) - - d = Z.D[1] #taking one of the decision nodes to retrieve the information_set_R - information_set_R = vec(collect(paths(S[d.I_j]))) - results = DataFrame(Information_set = map( x -> string(x) * "%", [0:1:100;])) - # T1 - Z_j = Z.Z_j[1] - probs = map(x -> x > 0 ? 1 : 0, get(sprobs.probs, 1,0)) #these are zeros and ones - dec = [Z_j(s_I) for s_I in information_set_R] - results[!, "T1"] = map(x -> x == 0 ? "" : "$x", probs.*dec) - - # T2 - Z_j = Z.Z_j[2] - probs = map(x -> x > 0 ? 1 : 0, (get(sprobs.probs, 4,0))) #these are zeros and ones - dec = [Z_j(s_I) for s_I in information_set_R] - results[!, "T2"] = map(x -> x == 0 ? "" : "$x", probs.*dec) - - # TD - Z_j = Z.Z_j[3] - probs = map(x -> x > 0 ? 1 : 0, (get(sprobs.probs, 6,0))) #these are zeros and ones - dec = [Z_j(s_I) for s_I in information_set_R] - results[!, "TD"] = map(x -> x == 0 ? "" : "$x", probs.*dec) - - pretty_table(results) -end - - -const R0 = 1 -const H = 2 -const T1 = 3 -const R1 = 4 -const T2 = 5 -const R2 = 6 -const TD = 7 -const TC = 8 -const HB = 9 - +@info("Creating the influence diagram.") +diagram = InfluenceDiagram() const H_states = ["CHD", "no CHD"] const T_states = ["TRS", "GRS", "no test"] const TD_states = ["treatment", "no treatment"] -const R_states = map( x -> string(x) * "%", [0:1:100;]) -const TC_states = ["TRS", "GRS", "TRS & GRS", "no tests"] -const HB_states = ["CHD & treatment", "CHD & no treatment", "no CHD & treatment", "no CHD & no treatment"] +const R_states = [string(x) * "%" for x in [0:1:100;]] -@info("Creating the influence diagram.") -S = States([ - (length(R_states), [R0, R1, R2]), - (length(H_states), [H]), - (length(T_states), [T1, T2]), - (length(TD_states), [TD]) -]) -C = Vector{ChanceNode}() -D = Vector{DecisionNode}() -V = Vector{ValueNode}() -X = Vector{Probabilities}() -Y = Vector{Consequences}() +add_node!(diagram, ChanceNode("R0", [], R_states)) +add_node!(diagram, ChanceNode("R1", ["R0", "H", "T1"], R_states)) +add_node!(diagram, ChanceNode("R2", ["R1", "H", "T2"], R_states)) +add_node!(diagram, ChanceNode("H", ["R0"], H_states)) +add_node!(diagram, DecisionNode("T1", ["R0"], T_states)) +add_node!(diagram, DecisionNode("T2", ["R1"], T_states)) +add_node!(diagram, DecisionNode("TD", ["R2"], TD_states)) -I_R0 = Vector{Node}() -X_R0 = zeros(S[R0]) -X_R0[chosen_risk_level] = 1 -push!(C, ChanceNode(R0, I_R0)) -push!(X, Probabilities(R0, X_R0)) +add_node!(diagram, ValueNode("TC", ["T1", "T2"])) +add_node!(diagram, ValueNode("HB", ["H", "TD"])) -I_H = [R0] -X_H = zeros(S[R0], S[H]) -X_H[:, 1] = data.risk_levels # 1 = "CHD" -X_H[:, 2] = 1 .- X_H[:, 1] # 2 = "no CHD" -push!(C, ChanceNode(H, I_H)) -push!(X, Probabilities(H, X_H)) +generate_arcs!(diagram) +X_R0 = ProbabilityMatrix(diagram, "R0") +set_probability!(X_R0, [chosen_risk_level], 1) +add_probabilities!(diagram, "R0", X_R0) -I_T1 = [R0] -push!(D, DecisionNode(T1, I_T1)) +X_H = ProbabilityMatrix(diagram, "H") +set_probability!(X_H, [:, "CHD"], data.risk_levels) +set_probability!(X_H, [:, "no CHD"], 1 .- data.risk_levels) +add_probabilities!(diagram, "H", X_H) -I_R1 = [R0, H, T1] -X_R1 = zeros(S[I_R1]..., S[R1]) +X_R = ProbabilityMatrix(diagram, "R1") for s_R0 = 1:101, s_H = 1:2, s_T1 = 1:3 - X_R1[s_R0, s_H, s_T1, :] = state_probabilities(update_risk_distribution(s_R0, s_T1), s_T1, s_H, s_R0) + X_R[s_R0, s_H, s_T1, :] = state_probabilities(update_risk_distribution(s_R0, s_T1), s_T1, s_H, s_R0) end -push!(C, ChanceNode(R1, I_R1)) -push!(X, Probabilities(R1, X_R1)) - - -I_T2 = [R1] -push!(D, DecisionNode(T2, I_T2)) +add_probabilities!(diagram, "R1", X_R) +add_probabilities!(diagram, "R2", X_R) -I_R2 = [H, R1, T2] -X_R2 = zeros(S[I_R2]..., S[R2]) -for s_R1 = 1:101, s_H = 1:2, s_T2 = 1:3 - X_R2[s_H, s_R1, s_T2, :] = state_probabilities(update_risk_distribution(s_R1, s_T2), s_T2, s_H, s_R1) -end -push!(C, ChanceNode(R2, I_R2)) -push!(X, Probabilities(R2, X_R2)) - - -I_TD = [R2] -push!(D, DecisionNode(TD, I_TD)) - - -I_TC = [T1, T2] -Y_TC = zeros(S[I_TC]...) cost_TRS = -0.0034645 cost_GRS = -0.004 -cost_forbidden = 0 #the cost of forbidden test combinations is negligible -Y_TC[1 , 1] = cost_forbidden -Y_TC[1 , 2] = cost_TRS + cost_GRS -Y_TC[1, 3] = cost_TRS -Y_TC[2, 1] = cost_GRS + cost_TRS -Y_TC[2, 2] = cost_forbidden -Y_TC[2, 3] = cost_GRS -Y_TC[3, 1] = cost_TRS -Y_TC[3, 2] = cost_GRS -Y_TC[3, 3] = 0 -push!(V, ValueNode(TC, I_TC)) -push!(Y, Consequences(TC, Y_TC)) - - -I_HB = [H, TD] -Y_HB = zeros(S[I_HB]...) -Y_HB[1 , 1] = 6.89713671259061 # sick & treat -Y_HB[1 , 2] = 6.65436854256236 # sick & don't treat -Y_HB[2, 1] = 7.64528451705134 # healthy & treat -Y_HB[2, 2] = 7.70088349200034 # healthy & don't treat -push!(V, ValueNode(HB, I_HB)) -push!(Y, Consequences(HB, Y_HB)) - - -@info("Validate influence diagram.") -validate_influence_diagram(S, C, D, V) -sort!.((C, D, V, X, Y), by = x -> x.j) - -P = DefaultPathProbability(C, X) -U = DefaultPathUtility(V, Y) +forbidden = 0 #the cost of forbidden test combinations is negligible +Y_TC = UtilityMatrix(diagram, "TC") +set_utility!(Y_TC, ["TRS", "TRS"], forbidden) +set_utility!(Y_TC, ["TRS", "GRS"], cost_TRS + cost_GRS) +set_utility!(Y_TC, ["TRS", "no test"], cost_TRS) +set_utility!(Y_TC, ["GRS", "TRS"], cost_TRS + cost_GRS) +set_utility!(Y_TC, ["GRS", "GRS"], forbidden) +set_utility!(Y_TC, ["GRS", "no test"], cost_GRS) +set_utility!(Y_TC, ["no test", "TRS"], cost_TRS) +set_utility!(Y_TC, ["no test", "GRS"], cost_GRS) +set_utility!(Y_TC, ["no test", "no test"], 0) +add_utilities!(diagram, "TC", Y_TC) + +Y_HB = UtilityMatrix(diagram, "HB") +set_utility!(Y_HB, ["CHD", "treatment"], 6.89713671259061) +set_utility!(Y_HB, ["CHD", "no treatment"], 6.65436854256236 ) +set_utility!(Y_HB, ["no CHD", "treatment"], 7.64528451705134) +set_utility!(Y_HB, ["no CHD", "no treatment"], 7.70088349200034) +add_utilities!(diagram, "HB", Y_HB) + +generate_diagram!(diagram) @info("Creating the decision model.") model = Model() -z = DecisionVariables(model, S, D) +z = DecisionVariables(model, diagram) # Defining forbidden paths to include all those where a test is repeated twice -forbidden_tests = ForbiddenPath[([T1,T2], Set([(1,1),(2,2),(3,1), (3,2)]))] +forbidden_tests = ForbiddenPath(diagram, ["T1","T2"], [("TRS", "TRS"),("GRS", "GRS"),("no test", "TRS"), ("no test", "GRS")]) +fixed_R0 = FixedPath(diagram, Dict("R0" => chosen_risk_level)) scale_factor = 10000.0 -x_s = PathCompatibilityVariables(model, z, S, P; fixed = Dict(1 => chosen_risk_level), forbidden_paths = forbidden_tests, probability_cut=false) +x_s = PathCompatibilityVariables(model, diagram, z; fixed = fixed_R0, forbidden_paths = [forbidden_tests], probability_cut=false) -EV = expected_value(model, x_s, U, P, probability_scale_factor = scale_factor) +EV = expected_value(model, diagram, x_s, probability_scale_factor = scale_factor) @objective(model, Max, EV) @info("Starting the optimization process.") @@ -268,25 +194,24 @@ optimizer = optimizer_with_attributes( "MIPGap" => 1e-6, ) set_optimizer(model, optimizer) +GC.enable(false) optimize!(model) +GC.enable(true) @info("Extracting results.") Z = DecisionStrategy(z) +S_probabilities = StateProbabilities(diagram, Z) +U_distribution = UtilityDistribution(diagram, Z) @info("Printing decision strategy using tailor made function:") -sprobs = StateProbabilities(S, P, Z) -analysing_results(Z, sprobs) +print_decision_strategy(diagram, Z, S_probabilities) @info("Printing state probabilities:") # Here we can see that the probability of having a CHD event is exactly that of the chosen risk level -print_state_probabilities(sprobs, [R0, R1, R2]) - -@info("Computing utility distribution.") -udist = UtilityDistribution(S, P, U, Z) +print_state_probabilities(diagram, S_probabilities, ["R0", "R1", "R2"]) @info("Printing utility distribution.") -print_utility_distribution(udist) +print_utility_distribution(U_distribution) -@info("Printing statistics") -print_statistics(udist) +print_statistics(U_distribution) From f335160eac13c6f446d84894d93438638c037837 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 31 Aug 2021 13:14:42 +0300 Subject: [PATCH 086/133] State to Int16 and edits in docstrings. --- src/influence_diagram.jl | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 722dad34..f14fc72f 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -57,9 +57,9 @@ end """ const State = Int -Primitive type for the number of states. Alias for `Int`. +Primitive type for the number of states. Alias for `Int16`. """ -const State = Int +const State = Int16 """ @@ -585,6 +585,9 @@ julia> set_probability!(X_O, ["lemon", "peach"], [0.2, 0.8]) julia> add_probabilities!(diagram, "O", X_O) julia> add_probabilities!(diagram, "O", [0.2, 0.8]) + +!!! note +The arcs must be generated before probabilities or utilities can be added to the influence diagram. ``` """ function add_probabilities!(diagram::InfluenceDiagram, node::Name, probabilities::AbstractArray{Float64, N}) where N @@ -729,6 +732,8 @@ julia> add_utilities!(diagram, "V3", Y_V3) julia> add_utilities!(diagram, "V1", [0, -25]) ``` +!!! note +The arcs must be generated before probabilities or utilities can be added to the influence diagram. """ function add_utilities!(diagram::InfluenceDiagram, node::Name, utilities::AbstractArray{T, N}) where {N,T<:Real} v = findfirst(x -> x==node, diagram.Names) @@ -780,16 +785,15 @@ end """ function generate_arcs!(diagram::InfluenceDiagram) -Generate arc structures using nodes added to influence diagram, by generating correct -values for the vectors Names, I_j, states, S, C, D, V in the influence digram. +Generate arc structures using nodes added to influence diagram, by ordering nodes, +giving them indices and generating correct values for the vectors Names, I_j, states, +S, C, D, V in the influence digram. Abstraction is created and the names of the nodes +and states are only used in the user interface from here on. # Examples ```julia julia> generate_arcs!(diagram) ``` - -!!! note -The arcs must be generated before probabilities or utilities can be added to the influence diagram. """ function generate_arcs!(diagram::InfluenceDiagram) From 77c7c9fb990a00e35e9463fbc58706886a7003f2 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 31 Aug 2021 17:11:58 +0300 Subject: [PATCH 087/133] Fixed bug by specifying types of X and Y in setindex functions. The unspecified types caused errors due to the .= broadcasting in some cases. --- src/influence_diagram.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index f14fc72f..0189c4df 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -485,7 +485,7 @@ end Base.size(PM::ProbabilityMatrix) = size(PM.matrix) Base.getindex(PM::ProbabilityMatrix, I::Vararg{Int,N}) where N = getindex(PM.matrix, I...) Base.setindex!(PM::ProbabilityMatrix, p::T, I::Vararg{Int,N}) where {N, T<:Real} = (PM.matrix[I...] = p) -Base.setindex!(PM::ProbabilityMatrix{N}, X, I::Vararg{Any, N}) where N = (PM.matrix[I...] .= X) +Base.setindex!(PM::ProbabilityMatrix{N}, X::Array{T}, I::Vararg{Any, N}) where {N, T<:Real} = (PM.matrix[I...] .= X) """ function ProbabilityMatrix(diagram::InfluenceDiagram, node::Name) @@ -630,7 +630,7 @@ end Base.size(UM::UtilityMatrix) = size(UM.matrix) Base.getindex(UM::UtilityMatrix, I::Vararg{Int,N}) where N = getindex(UM.matrix, I...) Base.setindex!(UM::UtilityMatrix, y::T, I::Vararg{Int,N}) where {N, T<:Real} = (UM.matrix[I...] = y) -Base.setindex!(UM::UtilityMatrix{N}, Y, I::Vararg{Any, N}) where N = (UM.matrix[I...] .= Y) +Base.setindex!(UM::UtilityMatrix{N}, Y::Array{T}, I::Vararg{Any, N}) where {N, T<:Real} = (UM.matrix[I...] .= Y) """ function UtilityMatrix(diagram::InfluenceDiagram, node::Name) From 485a064d41dd98a18c2bb063720b379790a121c3 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 31 Aug 2021 17:14:06 +0300 Subject: [PATCH 088/133] Updated usage page of documentation. --- docs/src/figures/2chance_1decision_1value.svg | 3 + docs/src/figures/usage.drawio | 1 + docs/src/usage.md | 274 +++++++++++++++--- 3 files changed, 240 insertions(+), 38 deletions(-) create mode 100644 docs/src/figures/2chance_1decision_1value.svg create mode 100644 docs/src/figures/usage.drawio diff --git a/docs/src/figures/2chance_1decision_1value.svg b/docs/src/figures/2chance_1decision_1value.svg new file mode 100644 index 00000000..1b4d05b4 --- /dev/null +++ b/docs/src/figures/2chance_1decision_1value.svg @@ -0,0 +1,3 @@ + + +
C1 \\ \{x, y,...
D1 \\ \{a,b\}
C2 \\ \{v,w\}
V
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/src/figures/usage.drawio b/docs/src/figures/usage.drawio new file mode 100644 index 00000000..8d5d492c --- /dev/null +++ b/docs/src/figures/usage.drawio @@ -0,0 +1 @@ +7Vldb5swFP01kbaHSWASkj4u6ZemTXuo1E17c+AC7gymxvmgv342thNoPppsWUi2pFGA4wu27z3n6FZ0vFE6v+M4T76wEGgHOeG84113EBr0ffmrgFIDPnI0EHMSashdAg/kBQxowyYkhKIRKBijguRNMGBZBoFoYJhzNmuGRYw2Z81xDCvAQ4DpKvqNhCIx2+o5S/weSJzYmV3HjKTYBptHFAkO2UxDVYx30/FGnDGhz9L5CKjKnc2LftDthtHFwjhkYpcb/MexX2ZP868/nj8Vxff8JSzRh55ZmyjthiGU+zeXGcvkYcjZJAtBPcaRV4yLhMUsw/QzY7kEXQk+gRClqR6eCCahRKTUjEaE0hGjjFdTeFFP/Um8EJz9hNqIX30WIzbjMlfD1e3avLIJD2DLHi1tMI9BbInzdJxKQG0Ck8w7YCkIXsoADhQLMm0SBBuexYu4ZSnkianGHpUxz51iOjEzdZBPhcpNjrNGzfzniSLRMNCJ/KjWGI/fSTrIr5zdqZ29X4bLs1gfu/I7clVQb6S++qQ/nOubSn14qcBrHW4XI/em12Of9ZpOlEqpKhbNEiLgIcdVrWbSLJocwUWu9RuRueLab5AmYpmo4foj8Rw4kUUBrmYnWWwYtQfHpsAFzLeywox2jfyN3SFrB7OaeRgoqfmGxQ5Oo+5/IHB0lgJHawSulHW9RohYKnDclN+rqv6husIeDMLuuooN0NirKtaiiroti8g7thejVQpM5U2ziwO/zZ2F5RryuKhl9rheGx68g6IP2mR1dzRht39SLmzXfemzzkvlHjqxRstt5V+pY6vcP1OV+5dma2cptd1tWe6sFmtNWwSyWNFhiyXd7RanhKp03AOdgiABNgNGl65KPaYkzuRFIFMPfE8NbqLEhsIfoqcenFpfNGjDMaMI/CBYV62wfzV2nEM75tWOjonQaTnm1aUvOse+yHdOrC+yLvNPq3zxauLMVL7g16UveltKbfdFljtHs+TH/ayWJywdT4q3rfaAHdYWNW9yhL/XYfX9o3VY8nL55rAaq71+9W5+AQ== \ No newline at end of file diff --git a/docs/src/usage.md b/docs/src/usage.md index c63393aa..6113d79c 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -1,61 +1,259 @@ # Usage -On this page, we demonstrate common patterns for expressing influence diagrams and creating decision models using `DecisionProgramming.jl`. We can import the package with the `using` keyword. +On this page, we demonstrate common patterns for expressing influence diagrams and creating decision models using `DecisionProgramming.jl`. We also discuss the abstraction that is created using the influence diagram structure. We can import the package with the `using` keyword. ```julia using DecisionProgramming ``` -## Chance Nodes -![](figures/chance-node.svg) +## Adding nodes +![](figures/2chance_1decision_1value.svg) -Given the above influence diagram, we can create the `ChanceNode` and `Probabilities` structures for the node `3` as follows: +Given the above influence diagram, we can express is as a Decision Programming model as follows. We create `ChanceNode` and `DecisionNode` instances and add them to the influence diagram. Creating a `ChanceNode` or `DecisionNode` requires us to give it a unique name, its information set and its states. If the node is a root node, the information set is left empty using square brackets. The order in which nodes are added does not matter. ```julia -S = States([2, 3, 2]) -j = 3 -I_j = Node[1, 2] -X_j = zeros(S[I_j]..., S[j]) -X_j[1, 1, :] = [0.1, 0.9] -X_j[1, 2, :] = [0.0, 1.0] -X_j[1, 3, :] = [0.3, 0.7] -X_j[2, 1, :] = [0.2, 0.8] -X_j[2, 2, :] = [0.4, 0.6] -X_j[2, 3, :] = [1.0, 0.0] -ChanceNode(j, I_j) -Probabilities(j, X_j) +diagram = InfluenceDiagram() +add_node!(diagram, DecisionNode("D1", [], ["a", "b"])) +add_node!(diagram, ChanceNode("C2", ["D1", "C1"], ["v", "w"])) +add_node!(diagram, ChanceNode("C1", [], ["x", "y", "z"])) +add_node!(diagram, ValueNode("V", ["C2"])) ``` +Once all the nodes are added, we generate the arcs. This function creates the abstraction of the influence diagram where each node is numbered, so that their predecessors have numbers less than theirs. In effect, the chance and decision nodes are numbered such that $C \cup D = \{1,...,n\}$, where $n = \mid C\mid + \mid D\mid$. The value nodes are numbered $V = \{n+1,..., N\}$, where $N = \mid C\mid + \mid D\mid + \mid V \mid$. For more details on influence diagrams see page [influence diagram](decision-programming/influence-diagram.md). +```julia +generate_arcs!(diagram) +``` +Now we can see that the influence diagram structures `Names`, `I_j`, `States`, `S`, `C`, `D` and `V` fields have been properly filled. Field Names holds all the names of nodes in the order of their numbers, from this we can see that node D1 has been number 1, node C1 number 2 and node C2 number 3. Field I_j holds the information sets of each node, and the nodes are identified by their numbers. Field States holds the names of the states of each node and field S holds the number of states each node has. Fields C, D and V hold the number of chance, decision and value nodes respectively. + +```julia +julia> diagram.Names +4-element Array{String,1}: + "D1" + "C1" + "C2" + "V" + +julia> diagram.I_j +4-element Array{Array{Int16,1},1}: + [] + [] + [1, 2] + [3] + +julia> diagram.States +3-element Array{Array{String,1},1}: + ["a", "b"] + ["x", "y", "z"] + ["v", "w"] + +julia> diagram.S +3-element States: + 2 + 3 + 2 + +julia> diagram.C +2-element Array{Int16,1}: + 2 + 3 + +julia> diagram.D +1-element Array{Int16,1}: + 1 + +julia> diagram.V +1-element Array{Int16,1}: + 4 +``` + +## Probability Matrices +Each chance node needs a probability matrix which describes the probability distribution over its states given an information state. It holds probability values +$$ℙ(X_j=s_j∣X_{I(j)}=𝐬_{I(j)})$$ + +for all $s_j \in S_j$ and $s_{I(j)} \in S_{I(j)}$. + +Thus, the probability matrix of a chance node needs to have dimensions that correspond to the number of states of the nodes in its information set and number of state of the node itself. + +For example, the node C1 in the influence diagram above has an empty information set and it has three states x, y, and z. Therefore its probability matrix needs dimensions (3,1). If the probabilities of x, y and z happening are $10\%, 30\%$ and $60\%$ then the probability matrix $X_{C1}$ should be $[0.1 \quad 0.3 \quad 0.6]$. The order of the probabilities is determined by the order in which the states were declared when adding the node. + +In Decision Programming the probability matrix is added in the following way. Note, that probability matrices can only be added after the arcs have been generated. + +```julia +# How C1 was added: add_node!(diagram, ChanceNode("C1", [], ["x", "y", "z"])) +X_C1 = [0.1, 0.3, 0.6] +add_probabilities!(diagram, "C1", X_C1) +``` + +The `add_probabilities!` function adds the probability matrix as a `Probabilities` structure into the influence diagram's `X` field. +```julia +julia> diagram.X +1-element Array{Probabilities,1}: + [0.1, 0.3, 0.6] +``` + + +As another example, we can look at the probability matrix of node C2 from the influence diagram above. It has two nodes in its information set: C1 and D1. These nodes have 3 and 2 state respectively. Node C2 itself has 2 states. The dimensions of the probability matrix should alway be $(\bold{S}_{I(j)}, S_j)$. The question however should it be (3, 2, 2) or (2, 3, 2)? The dimensions corresponding to the nodes in information set, should be in ascending order of the nodes' numbers. This is also the order in which they are in `diagram.I_j`. In this case the influence diagram looks like this: +```julia +julia> diagram.Names +4-element Array{String,1}: + "D1" + "C1" + "C2" + "V" + + julia> diagram.I_j +4-element Array{Array{Int16,1},1}: + [] + [] + [1, 2] + [3] + + julia> diagram.S +3-element States: + 2 + 3 + 2 +``` + +Therefore, the probability matrix of node C2 should have dimensions (2, 3, 2). The probability matrix can be added by initialising the matrix and then filling in its elements as shown below. +```julia +X_C2 = zeros(2, 3, 2) +X_C2[1, 1, 1] = ... +X_C2[1, 1, 2] = ... +X_C2[1, 1, 2] = ... +⋮ +add_probabilities!(diagram, "C2", X_C2) +``` +It is crucial to understand what the matrix indices represent. Similarly as with the nodes in diagram.Names, states of a node are numbered according to their position in diagram.States vector of the node in question. The position of an element in the matrix represents a subpath in the influence diagram. However, the states in the path are referred to with their numbers instead of with their names. The order of the states in diagram.States of each node is seen below. From this we see that for nodes D1, C1, C2 the subpath `(1,1,1)` is $(a, x, v)$ and subpath `(1, 3, 2)` is $(a, z, w)$. +Therefore, the probability value at `X_C2[1, 3, 2]` should be the probability of the scenario $(a, z, w)$ occuring. +```julia +julia> diagram.States +3-element Array{Array{String,1},1}: + ["a", "b"] + ["x", "y", "z"] + ["v", "w"] +``` +### Helper Syntax +Figuring out the dimensions of a probability matrix and adding the probability values into in the way that was described above is difficult. Therefore, we have implemented an easier syntax. + +A probability matrix can be initialised with the correct dimensions using the `ProbabilityMatrix` function. It initiliases the probability matrix with zeros. +```julia +julia> X_C2 = ProbabilityMatrix(diagram, "C2") +2×3×2 ProbabilityMatrix{3}: +[:, :, 1] = + 0.0 0.0 0.0 + 0.0 0.0 0.0 + +[:, :, 2] = + 0.0 0.0 0.0 + 0.0 0.0 0.0 -## Decision Nodes -![](figures/decision-node.svg) +julia> size(X_C2) +(2, 3, 2) +``` +A matrix of type `ProbabilityMatrix` can be filled using the names of the states. Notice that if we use the `Colon` (`:`) to indicate several elements of the matrix, the probability values have to be again given in the order of the states in the vector `diagram.States`. +```julia +julia> set_probability!(X_C2, ["a", "z", "w"], 0.25) +0.25 + +julia> set_probability!(X_C2, ["a", "z", "v"], 0.75) +0.75 + +julia> set_probability!(X_C2, ["a", "x", :], [0.3, 0.7]) +2-element Array{Float64,1}: + 0.3 + 0.7 +``` + +A matrix of type `ProbabilityMatrix` can also be filled using the matrix indices if that is more convient. This is the same as above, only using the number indices instead of the state names. +```julia +julia> X_C2[1, 3, 2] = 0.25 +0.25 + +julia> X_C2[1, 3, 1] = 0.75 +0.75 + +julia> X_C2[1, 1, :] = [0.3, 0.7] +2-element Array{Float64,1}: + 0.3 + 0.7 +```` -Given the above influence diagram, we can create the `DecisionNode` structure for the node `3` as follows: +The probability matrix X_C2 now has elements with probability values. +```julia +julia> X_C2 +2×3×2 ProbabilityMatrix{3}: +[:, :, 1] = + 0.3 0.0 0.75 + 0.0 0.0 0.0 + +[:, :, 2] = + 0.7 0.0 0.25 + 0.0 0.0 0.0 +``` +The probability matrix can be added to the diagram once the correct probability values have been added into it. The probability matrix of node C2 is added exactly like before, despite X_C2 now being a matrix of type `ProbabilityMatrix`. ```julia -S = States([2, 3, 2]) -j = 3 -I_j = Node[1, 2] -DecisionNode(j, I_j) +julia> add_probabilities!(diagram, "C2", X_C2) +``` + +## Utility Matrices +Each value node maps its information states to utility values. In Decision Programming the utility values are passed to the influence diagram using utility matrices. Utility matrices are very similar to probability matrices of chance nodes. There are only two important differences. First, the utility matrices hold utility values instead of probabilities, meaning that they do not need to sum to one. Second, since value nodes do not have states, the utility matrix cardinality only depends on the number of states of the nodes in the information set have. + +As an example, the utility matrix of node V should have dimensions (2,1) because its information set consists of node C2, which has two states. If state $v$ of node C2 yields a utility of -100 and state $w$ yields utility of 400, then the utility matrix of node V can be added in the following way. Note, that utility matrices can only be added after the arcs have been generated. + +```julia +julia> Y_V = zeros(2) +2-element Array{Float64,1}: + 0.0 + 0.0 + +julia> Y_V[1] = -100 +-100 + +julia> Y_V[2] = 400 +400 + +julia> add_utilities!(diagram, "V", Y_V) ``` -## Value Nodes -![](figures/value-node.svg) +The other option is to add the utility matrix using the `UtilityMatrix` type. This is very similar to the `ProbabilityMatrix` type. The `UtilityMatrix` function initialises the values to `Inf`. Using the `UtilityMatrix` type's functionalities, the utility matrix of node V could also be added like shown below. This achieves the exact same result as we did above with the more abstract syntax. + + +```julia +julia> Y_V = UtilityMatrix(diagram, "V") +2-element UtilityMatrix{1}: + Inf + Inf + +julia> set_utility!(Y_V, ["w"], 400) +400 + +julia> set_utility!(Y_V, ["v"], -100) +-100 + +julia> add_utilities!(diagram, "V", Y_V) +``` + +The `add_utilities!` function adds the utility matrix as a `Utilities` structure into the influence diagram's `Y` field. +```julia +julia> diagram.Y +1-element Array{Utilities,1}: + [-100.0, 400.0] +``` -Given the above influence diagram, we can create `ValueNode` and `Consequences` structures for node `3` as follows: +## Generating the influence diagram +The final part of modeling an influence diagram using the Decision Programming package is generating the full influence diagram. This is done using the `generate_diagram!` function. ```julia -S = States([2, 3]) -j = 3 -I_j = [1, 2] -Y_j = zeros(S[I_j]...) -Y_j[1, 1] = -1.3 -Y_j[1, 2] = 2.5 -Y_j[1, 3] = 0.1 -Y_j[2, 1] = 0.0 -Y_j[2, 2] = 3.2 -Y_j[2, 3] = -2.7 -ValueNode(j, I_j) -Consequences(j, Y_j) +generate_diagram!(diagram) ``` +In this function, first, the probability and utility matrices are sorted according to the chance and value nodes' indices into the influence diagram's `X` and `Y` fields respectively. + +Second, the path probability and path utility types are set. These types define how the path probability $p(𝐬)$ and $\mathcal{U}(𝐬)$ are defined in the model. By default, the default path probability and default path utility are used. See the [influence diagram](decision-programming/influence-diagram.md) for more information on these. + + + + + From 8eec0977566f265bed59eba7a7c0e38d35b37fda Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 31 Aug 2021 17:17:42 +0300 Subject: [PATCH 089/133] Updated code and documentation of n-monitoring example. --- docs/src/examples/n-monitoring.md | 279 +++++++++++++++--------------- examples/n_monitoring.jl | 177 ++++--------------- 2 files changed, 166 insertions(+), 290 deletions(-) diff --git a/docs/src/examples/n-monitoring.md b/docs/src/examples/n-monitoring.md index 4ff0145e..1ecd6999 100644 --- a/docs/src/examples/n-monitoring.md +++ b/docs/src/examples/n-monitoring.md @@ -8,7 +8,7 @@ The $N$-monitoring problem is described in [^1], sections 4.1 and 6.1. The influence diagram of the generalized $N$-monitoring problem where $N≥1$ and indices $k=1,...,N.$ The nodes are associated with states as follows. **Load state** $L=\{high, low\}$ denotes the load on a structure, **report states** $R_k=\{high, low\}$ report the load state to the **action states** $A_k=\{yes, no\}$ which represent different decisions to fortify the structure. The **failure state** $F=\{failure, success\}$ represents whether or not the (fortified) structure fails under the load $L$. Finally, the utility at target $T$ depends on the whether $F$ fails and the fortification costs. -We draw the cost of fortification $c_k∼U(0,1)$ from a uniform distribution, and the magnitude of fortification is directly proportional to the cost. Fortification is defined as +We begin by choosing $N$ and defining our fortification cost function. We draw the cost of fortification $c_k∼U(0,1)$ from a uniform distribution, and the magnitude of fortification is directly proportional to the cost. Fortification is defined as $$f(A_k=yes) = c_k$$ @@ -19,53 +19,67 @@ using Logging, Random using JuMP, Gurobi using DecisionProgramming -Random.seed!(13) - const N = 4 -const L = [1] -const R_k = [k + 1 for k in 1:N] -const A_k = [(N + 1) + k for k in 1:N] -const F = [2*N + 2] -const T = [2*N + 3] -const L_states = ["high", "low"] -const R_k_states = ["high", "low"] -const A_k_states = ["yes", "no"] -const F_states = ["failure", "success"] + +Random.seed!(13) const c_k = rand(N) const b = 0.03 fortification(k, a) = [c_k[k], 0][a] +``` + +### Initialise Influence Diagram +We initialise the influence diagram before adding nodes to it. -S = States([ - (length(L_states), L), - (length(R_k_states), R_k), - (length(A_k_states), A_k), - (length(F_states), F) -]) -C = Vector{ChanceNode}() -D = Vector{DecisionNode}() -V = Vector{ValueNode}() -X = Vector{Probabilities}() -Y = Vector{Consequences}() +```julia +diagram = InfluenceDiagram ``` -### Load State Probability -The probability that the load is high, $ℙ(L=high)$, is drawn from a uniform distribution. +### Add Nodes +Add node $L$ which represents the load on the structure. This node is the root node and thus, has an empty information set. Its states describe the state of the load, they are $high$ and $low$. -$$ℙ(L=high)∼U(0,1)$$ +```julia +add_node!(diagram, ChanceNode("L", [], ["high", "low"])) +``` + +The report nodes $R_k$ and action nodes $A_k$ are easily added with a for-loop. The report nodes have node $L$ in their information sets and their states are $high$ and $low$. The actions are made based on these report, which is represented by the $R_k$ nodes forming the information sets of the $A_k$ nodes. The action nodes have states $yes$ and $no$, which represents decisions to either fortify the structure or not. ```julia -for j in L - I_j = Vector{Node}() - X_j = zeros(S[I_j]..., S[j]) - X_j[1] = rand() - X_j[2] = 1.0 - X_j[1] - push!(C, ChanceNode(j, I_j)) - push!(X, Probabilities(j, X_j)) +for i in 1:N + add_node!(diagram, ChanceNode("R$i", ["L"], ["high", "low"])) + add_node!(diagram, DecisionNode("A$i", ["R$i"], ["yes", "no"])) end ``` -### Reporting Probability -The probabilities of the report states correspond to the load state. We draw the values $x∼U(0,1)$ and $y∼U(0,1)$ from uniform distribution. +The failure node $F$ has the load node $L$ and all of the action nodes $A_k$ in its information set. The failure node has states $failure$ and $success$. +```julia +add_node!(diagram, ChanceNode("F", ["L", ["A$i" for i in 1:N]...], ["failure", "success"])) +``` + +The value node $T$ is added as follows. +```julia +add_node!(diagram, ValueNode("T", ["F", ["A$i" for i in 1:N]...])) +``` + +### Generate arcs +Now that all of the nodes have been added to the influence diagram we generate the arcs between the nodes. This step automatically orders the nodes, gives them indices and reorganises the information into the appropriate form. +```julia +generate_arcs!(diagram) +``` + + +### Load State Probabilities +After generating the arcs, the probabilities and utilities can be added. The probability that the load is high, $ℙ(L=high)$, is drawn from a uniform distribution. For different syntax options for adding probabilities and utilities, see the [usage page](../usage.md). + +$$ℙ(L=high)∼U(0,1)$$ + +```julia +X_L = [rand(), 0] +X_L[2] = 1.0 - X_L[1] +add_probabilities!(diagram, "L", X_L) +``` + +### Reporting Probabilities +The probabilities of the report states correspond to the load state. We draw the values $x∼U(0,1)$ and $y∼U(0,1)$ from uniform distributions. $$ℙ(R_k=high∣L=high)=\max\{x,1-x\}$$ @@ -73,124 +87,110 @@ $$ℙ(R_k=low∣L=low)=\max\{y,1-y\}$$ The probability of a correct report is thus in the range [0.5,1]. (This reflects the fact that a probability under 50% would not even make sense, since we would notice that if the test suggests a high load, the load is more likely to be low, resulting in that a low report "turns into" a high report and vice versa.) +In Decision Programming we add these probabilities in the following way. The probability matrix of an $R$ node is of dimensions (2,2), where the rows correspond to the states $high$ and $low$ of its predecessor node $L$ and the columns to its states $high$ and $low$. The probability matrix is declared and added in the following way. + ```julia -for j in R_k - I_j = L +for i in 1:N x, y = rand(2) - X_j = zeros(S[I_j]..., S[j]) - X_j[1, 1] = max(x, 1-x) - X_j[1, 2] = 1.0 - X_j[1, 1] - X_j[2, 2] = max(y, 1-y) - X_j[2, 1] = 1.0 - X_j[2, 2] - push!(C, ChanceNode(j, I_j)) - push!(X, Probabilities(j, X_j)) + X_R = ProbabilityMatrix(diagram, "R$i") + set_probability!(X_R, ["high", "high"], max(x, 1-x)) + set_probability!(X_R, ["high", "low"], 1-max(x, 1-x)) + set_probability!(X_R, ["low", "low"], max(y, 1-y)) + set_probability!(X_R, ["low", "high"], 1-max(y, 1-y)) + add_probabilities!(diagram, "R$i", X_R) end ``` -### Decision to Fortify +### Probability of Failure +The probability of failure is decresead by fortification actions. We draw the values $x∼U(0,1)$ and $y∼U(0,1)$ from uniform distribution. -Only the corresponding load report is known when making the fortification decision, thus $I(A_k)=R_k$. +$$ℙ(F=failure∣A_N,...,A_1,L=high)=\frac{\max{\{x, 1-x\}}}{\exp{(b ∑_{k=1,...,N} f(A_k))}}$$ +$$ℙ(F=failure∣A_N,...,A_1,L=low)=\frac{\min{\{y, 1-y\}}}{\exp{(b ∑_{k=1,...,N} f(A_k))}}$$ + +First we initialise the probability matrix for node $F$. ```julia -for (i, j) in zip(R_k, A_k) - I_j = [i] - push!(D, DecisionNode(j, I_j)) -end +X_F = ProbabilityMatrix(diagram, "F") ``` -### Probability of Failure -The probabilities of failure which are decresead by fortifications. We draw the values $x∼U(0,1)$ and $y∼U(0,1)$ from uniform distribution. +This matrix has dimensions $(2, \textcolor{orange}{2, 2, 2, 2}, 2)$. This is because its information set consists of node $L$ and nodes $A_k$ all of which have 2 states and the node $F$ itself has 2 states. The orange colored dimensions correspond to the $A_k$ states, there are four of them because in this case $N = 4$. -$$ℙ(F=failure∣A_N,...,A_1,L=high)=\frac{\max{\{x, 1-x\}}}{\exp{(b ∑_{k=1,...,N} f(A_k))}}$$ +To set the probabilities we have to iterate over the information states. Here it helps to know that in Decision Programming the states of each node are mapped to numbers in the back-end. For instance, the load states $high$ and $low$ are referred to as 1 and 2. The same applies for the action states $yes$ and $no$, they are states 1 and 2. The `paths` function allows us to iterate over the paths of a specific nodes. In these paths, the states are refer to by their indices. Now we can iterate over the information states and set the probabilities into the probability matrix in the following way. -$$ℙ(F=failure∣A_N,...,A_1,L=low)=\frac{\min{\{y, 1-y\}}}{\exp{(b ∑_{k=1,...,N} f(A_k))}}$$ ```julia -for j in F - I_j = L ∪ A_k - x, y = rand(2) - X_j = zeros(S[I_j]..., S[j]) - for s in paths(S[A_k]) - d = exp(b * sum(fortification(k, a) for (k, a) in enumerate(s))) - X_j[1, s..., 1] = max(x, 1-x) / d - X_j[1, s..., 2] = 1.0 - X_j[1, s..., 1] - X_j[2, s..., 1] = min(y, 1-y) / d - X_j[2, s..., 2] = 1.0 - X_j[2, s..., 1] - end - push!(C, ChanceNode(j, I_j)) - push!(X, Probabilities(j, X_j)) +x_F, y_F = rand(2) +for s in paths([State(2) for i in 1:N]) + denominator = exp(b * sum(fortification(k, a) for (k, a) in enumerate(s))) + X_F[1, s..., 1] = max(x_F, 1-x_F) / denominator + X_F[1, s..., 2] = 1.0 - X_F[1, s..., 1] + X_F[2, s..., 1] = min(y_F, 1-y_F) / denominator + X_F[2, s..., 2] = 1.0 - X_F[2, s..., 1] end ``` -### Consequences -Utility from consequences at target $T$ from failure state $F$ +We add the probability matrix to the influence diagram. +```julia +add_probabilities!(diagram, "F", X_F) +``` + +### Utility +The utility from the different scenarios of the failure state at target $T$ are $$g(F=failure) = 0$$ -$$g(F=success) = 100$$ +$$g(F=success) = 100$$. -Utility from consequences at target $T$ from action states $A_k$ is +Utilities from the action states $A_k$ at target $T$ from are $$f(A_k=yes) = c_k$$ -$$f(A_k=no) = 0$$ +$$f(A_k=no) = 0$$. -Total cost +The total cost is thus -$$Y(F, A_N, ..., A_1) = g(F) + (-f(A_N)) + ... + (-f(A_1))$$ +$$Y(F, A_N, ..., A_1) = g(F) + (-f(A_N)) + ... + (-f(A_1))$$. +We first declare the utility matrix for node $T$. ```julia -for j in T - I_j = A_k ∪ F - Y_j = zeros(S[I_j]...) - for s in paths(S[A_k]) - cost = sum(-fortification(k, a) for (k, a) in enumerate(s)) - Y_j[s..., 1] = cost + 0 - Y_j[s..., 2] = cost + 100 - end - push!(V, ValueNode(j, I_j)) - push!(Y, Consequences(j, Y_j)) -end +Y_T = UtilityMatrix(diagram, "T") ``` - -### Validating the Influence Diagram - -Finally, we need to validate the influence diagram and sort the nodes, probabilities and consequences in increasing order by the node indices. +This matrix has dimensions $(2, \textcolor{orange}{2, 2, 2, 2})$, where again the twos correspond to the numbers of states the nodes in the information set have. Similarly as before, the first dimension corresponds to the states of node $F$ and the other 4 dimensions (in oragne) correspond to the states of the $A_k$ nodes. The utilities are set and added in the following way. ```julia -validate_influence_diagram(S, C, D, V) -sort!.((C, D, V, X, Y), by = x -> x.j) +for s in paths([State(2) for i in 1:N]) + cost = sum(-fortification(k, a) for (k, a) in enumerate(s)) + Y_T[1, s...] = 0 + cost + Y_T[2, s...] = 100 + cost +end +add_utilities!(diagram, "T", Y_T) ``` -We define the path probability. -```julia -P = DefaultPathProbability(C, X) -``` +### Generate Influence Diagram +The full influence diagram can now be generated. We use the default path probabilities and utilities, which are the default setting in this function. In the [Contingent Portfolio Programming](contingent-portfolio-programming.md) example, we show how to use a user-defined custom path utility function. + +In this particular problem, some of the path utilities are negative. In this case, we choose to use the [positive path utility](../decision-programming/decision-model.md) transformation, which translates the path utilities to positive values. This allows us to exclude the probability cut in the next section. -As the path utility, we use the default, which is the sum of the consequences given the path. ```julia -U = DefaultPathUtility(V, Y) +generate_diagram!(diagram, positive_path_utility = true) ``` - ## Decision Model - -An affine transformation is applied to the path utility, making all utilities positive. See the [section](../decision-programming/decision-model.md) on positive path utilities for details. +We initialise the JuMP model and declare the decision and path compatibility variables. Since we applied an affine transformation to the utility function, the probability cut can be excluded from the model formulation. ```julia -U⁺ = PositivePathUtility(S, U) model = Model() -z = DecisionVariables(model, S, D) -x_s = PathCompatibilityVariables(model, z, S, P, probability_cut = false) +z = DecisionVariables(model, diagram) +x_s = PathCompatibilityVariables(model, diagram, z, probability_cut = false) ``` -` The expected utility is used as the objective and the problem is solved using Gurobi. ```julia -EV = expected_value(model, π_s, U⁺) +EV = expected_value(model, diagram, x_s) @objective(model, Max, EV) + optimizer = optimizer_with_attributes( () -> Gurobi.Optimizer(Gurobi.Env()), "IntFeasTol" => 1e-9, @@ -202,48 +202,47 @@ optimize!(model) ## Analyzing Results -The decision strategy shows us that the optimal strategy is to make all four fortifications regardless of the reports (state 1 in fortification nodes corresponds to the option "yes"). +We obtain the decision strategy, state probabilities and utility distribution from the solution. ```julia Z = DecisionStrategy(z) +U_distribution = UtilityDistribution(diagram, Z) +S_probabilities = StateProbabilities(diagram, Z) ``` +The decision strategy shows us that the optimal strategy is to make all four fortifications regardless of the reports. + ```julia-repl -julia> print_decision_strategy(S, Z) -┌────────┬──────┬───┐ -│ Nodes │ (2,) │ 6 │ -├────────┼──────┼───┤ -│ States │ (1,) │ 1 │ -│ States │ (2,) │ 1 │ -└────────┴──────┴───┘ -┌────────┬──────┬───┐ -│ Nodes │ (3,) │ 7 │ -├────────┼──────┼───┤ -│ States │ (1,) │ 1 │ -│ States │ (2,) │ 1 │ -└────────┴──────┴───┘ -┌────────┬──────┬───┐ -│ Nodes │ (4,) │ 8 │ -├────────┼──────┼───┤ -│ States │ (1,) │ 1 │ -│ States │ (2,) │ 1 │ -└────────┴──────┴───┘ -┌────────┬──────┬───┐ -│ Nodes │ (5,) │ 9 │ -├────────┼──────┼───┤ -│ States │ (1,) │ 1 │ -│ States │ (2,) │ 1 │ -└────────┴──────┴───┘ +julia> print_decision_strategy(diagram, Z, S_probabilities) +┌────────────────┬────────────────┐ +│ State(s) of R1 │ Decision in A1 │ +├────────────────┼────────────────┤ +│ high │ yes │ +│ low │ yes │ +└────────────────┴────────────────┘ +┌────────────────┬────────────────┐ +│ State(s) of R2 │ Decision in A2 │ +├────────────────┼────────────────┤ +│ high │ yes │ +│ low │ yes │ +└────────────────┴────────────────┘ +┌────────────────┬────────────────┐ +│ State(s) of R3 │ Decision in A3 │ +├────────────────┼────────────────┤ +│ high │ yes │ +│ low │ yes │ +└────────────────┴────────────────┘ +┌────────────────┬────────────────┐ +│ State(s) of R4 │ Decision in A4 │ +├────────────────┼────────────────┤ +│ high │ yes │ +│ low │ yes │ +└────────────────┴────────────────┘ ``` The state probabilities for the strategy $Z$ can also be obtained. These tell the probability of each state in each node, given the strategy $Z$. - -```julia -sprobs = StateProbabilities(S, P, Z) -``` - ```julia-repl julia> print_state_probabilities(sprobs, L) ┌───────┬──────────┬──────────┬─────────────┐ @@ -283,12 +282,8 @@ julia> print_state_probabilities(sprobs, F) We can also print the utility distribution for the optimal strategy and some basic statistics for the distribution. -```julia -udist = UtilityDistribution(S, P, U, Z) -``` - ```julia-repl -julia> print_utility_distribution(udist) +julia> print_utility_distribution(U_distribution) ┌───────────┬─────────────┐ │ Utility │ Probability │ │ Float64 │ Float64 │ @@ -299,7 +294,7 @@ julia> print_utility_distribution(udist) ``` ```julia-repl -julia> print_statistics(udist) +julia> print_statistics(U_distribution) ┌──────────┬────────────┐ │ Name │ Statistics │ │ String │ Float64 │ diff --git a/examples/n_monitoring.jl b/examples/n_monitoring.jl index 1ddce2ff..64460e1e 100644 --- a/examples/n_monitoring.jl +++ b/examples/n_monitoring.jl @@ -30,33 +30,34 @@ X_L[2] = 1.0 - X_L[1] add_probabilities!(diagram, "L", X_L) for i in 1:N - x, y = rand(2) - X_R = zeros(2,2) - X_R[1, 1] = max(x, 1-x) - X_R[1, 2] = 1.0 - X_R[1, 1] - X_R[2, 2] = max(y, 1-y) - X_R[2, 1] = 1.0 - X_R[2, 2] + x_R, y_R = rand(2) + X_R = ProbabilityMatrix(diagram, "R$i") + set_probability!(X_R, ["high", "high"], max(x_R, 1-x_R)) + set_probability!(X_R, ["high", "low"], 1 - max(x_R, 1-x_R)) + set_probability!(X_R, ["low", "low"], max(y_R, 1-y_R)) + set_probability!(X_R, ["low", "high"], 1-max(y_R, 1-y_R)) add_probabilities!(diagram, "R$i", X_R) end -for i in [1] - x, y = rand(2) - X_F = zeros(2, [2 for i in 1:N]..., 2) - for s in paths([2 for i in 1:N]) - d = exp(b * sum(fortification(k, a) for (k, a) in enumerate(s))) - X_F[1, s..., 1] = max(x, 1-x) / d - X_F[1, s..., 2] = 1.0 - X_F[1, s..., 1] - X_F[2, s..., 1] = min(y, 1-y) / d - X_F[2, s..., 2] = 1.0 - X_F[2, s..., 1] - end - add_probabilities!(diagram, "F", X_F) + +X_F = ProbabilityMatrix(diagram, "F") +x_F, y_F = rand(2) +for s in paths([State(2) for i in 1:N]) + denominator = exp(b * sum(fortification(k, a) for (k, a) in enumerate(s))) + s1 = [s...] + X_F[1, s1..., 1] = max(x_F, 1-x_F) / denominator + X_F[1, s..., 2] = 1.0 - X_F[1, s..., 1] + X_F[2, s..., 1] = min(y_F, 1-y_F) / denominator + X_F[2, s..., 2] = 1.0 - X_F[2, s..., 1] end +add_probabilities!(diagram, "F", X_F) + Y_T = UtilityMatrix(diagram, "T") -for s in paths([2 for i in 1:N]) +for s in paths([State(2) for i in 1:N]) cost = sum(-fortification(k, a) for (k, a) in enumerate(s)) - Y_T[1, s...] = cost + 0 - Y_T[2, s...] = cost + 100 + Y_T[1, s...] = 0 + cost + Y_T[2, s...] = 100 + cost end add_utilities!(diagram, "T", Y_T) @@ -80,139 +81,19 @@ optimize!(model) @info("Extracting results.") Z = DecisionStrategy(z) U_distribution = UtilityDistribution(diagram, Z) -state_probabilities = StateProbabilities(diagram, Z) +S_probabilities = StateProbabilities(diagram, Z) @info("Printing decision strategy:") -print_decision_strategy(diagram, Z, state_probabilities) +print_decision_strategy(diagram, Z, S_probabilities) + +@info("State probabilities:") +print_state_probabilities(diagram, S_probabilities, ["L"]) +print_state_probabilities(diagram, S_probabilities, [["R$i" for i in 1:N]...]) +print_state_probabilities(diagram, S_probabilities, [["A$i" for i in 1:N]...]) +print_state_probabilities(diagram, S_probabilities, ["F"]) @info("Printing utility distribution.") print_utility_distribution(U_distribution) @info("Printing statistics") print_statistics(U_distribution) - -#= -const N = 4 -const L = [1] -const R_k = [k + 1 for k in 1:N] -const A_k = [(N + 1) + k for k in 1:N] -const F = [2*N + 2] -const T = [2*N + 3] -const L_states = ["high", "low"] -const R_k_states = ["high", "low"] -const A_k_states = ["yes", "no"] -const F_states = ["failure", "success"] -const c_k = rand(N) -const b = 0.03 -fortification(k, a) = [c_k[k], 0][a] - -@info("Creating the influence diagram.") -S = States([ - (length(L_states), L), - (length(R_k_states), R_k), - (length(A_k_states), A_k), - (length(F_states), F) -]) -C = Vector{ChanceNode}() -D = Vector{DecisionNode}() -V = Vector{ValueNode}() -X = Vector{Probabilities}() -Y = Vector{Consequences}() - -for j in L - I_j = Vector{Node}() - X_j = zeros(S[I_j]..., S[j]) - X_j[1] = rand() - X_j[2] = 1.0 - X_j[1] - push!(C, ChanceNode(j, I_j)) - push!(X, Probabilities(j, X_j)) -end - -for j in R_k - I_j = L - x, y = rand(2) - X_j = zeros(S[I_j]..., S[j]) - X_j[1, 1] = max(x, 1-x) - X_j[1, 2] = 1.0 - X_j[1, 1] - X_j[2, 2] = max(y, 1-y) - X_j[2, 1] = 1.0 - X_j[2, 2] - push!(C, ChanceNode(j, I_j)) - push!(X, Probabilities(j, X_j)) -end - -for (i, j) in zip(R_k, A_k) - I_j = [i] - push!(D, DecisionNode(j, I_j)) -end - -for j in F - I_j = L ∪ A_k - x, y = rand(2) - X_j = zeros(S[I_j]..., S[j]) - for s in paths(S[A_k]) - d = exp(b * sum(fortification(k, a) for (k, a) in enumerate(s))) - X_j[1, s..., 1] = max(x, 1-x) / d - X_j[1, s..., 2] = 1.0 - X_j[1, s..., 1] - X_j[2, s..., 1] = min(y, 1-y) / d - X_j[2, s..., 2] = 1.0 - X_j[2, s..., 1] - end - push!(C, ChanceNode(j, I_j)) - push!(X, Probabilities(j, X_j)) -end - -for j in T - I_j = A_k ∪ F - Y_j = zeros(S[I_j]...) - for s in paths(S[A_k]) - cost = sum(-fortification(k, a) for (k, a) in enumerate(s)) - Y_j[s..., 1] = cost + 0 - Y_j[s..., 2] = cost + 100 - end - push!(V, ValueNode(j, I_j)) - push!(Y, Consequences(j, Y_j)) -end - -validate_influence_diagram(S, C, D, V) -sort!.((C, D, V, X, Y), by = x -> x.j) - -P = DefaultPathProbability(C, X) -U = DefaultPathUtility(V, Y) - -@info("Creating the decision model.") -U⁺ = PositivePathUtility(S, U) -model = Model() -z = DecisionVariables(model, S, D) -x_s = PathCompatibilityVariables(model, z, S, P, probability_cut = false) -EV = expected_value(model, x_s, U⁺, P) -@objective(model, Max, EV) - -@info("Starting the optimization process.") -optimizer = optimizer_with_attributes( - () -> Gurobi.Optimizer(Gurobi.Env()), - "IntFeasTol" => 1e-9, -) -set_optimizer(model, optimizer) -optimize!(model) - -@info("Extracting results.") -Z = DecisionStrategy(z) - -@info("Printing decision strategy:") -print_decision_strategy(S, Z) - -@info("Printing state probabilities:") -sprobs = StateProbabilities(S, P, Z) -print_state_probabilities(sprobs, L) -print_state_probabilities(sprobs, R_k) -print_state_probabilities(sprobs, A_k) -print_state_probabilities(sprobs, F) - -@info("Computing utility distribution.") -udist = UtilityDistribution(S, P, U, Z) - -@info("Printing utility distribution.") -print_utility_distribution(udist) - -@info("Printing statistics") -print_statistics(udist) -=# From b5e0ee72ed74c69fe2ba5b02381f706bc4a8502b Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 31 Aug 2021 17:19:39 +0300 Subject: [PATCH 090/133] Deleted images used in the old usage page. --- docs/src/figures/chance-node.svg | 3 --- docs/src/figures/decision-node.svg | 3 --- docs/src/figures/getting-started.drawio | 1 - docs/src/figures/value-node.svg | 3 --- 4 files changed, 10 deletions(-) delete mode 100644 docs/src/figures/chance-node.svg delete mode 100644 docs/src/figures/decision-node.svg delete mode 100644 docs/src/figures/getting-started.drawio delete mode 100644 docs/src/figures/value-node.svg diff --git a/docs/src/figures/chance-node.svg b/docs/src/figures/chance-node.svg deleted file mode 100644 index 019a220b..00000000 --- a/docs/src/figures/chance-node.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
2 \\ \{1,2,3\...
1 \\ \{1,2\}
3 \\ \{1,2\}
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/src/figures/decision-node.svg b/docs/src/figures/decision-node.svg deleted file mode 100644 index c96f4bab..00000000 --- a/docs/src/figures/decision-node.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
2 \\ \{1,2,3\...
1 \\ \{1,2\}
3 \\ \{1,2\}
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/src/figures/getting-started.drawio b/docs/src/figures/getting-started.drawio deleted file mode 100644 index e3bc6a09..00000000 --- a/docs/src/figures/getting-started.drawio +++ /dev/null @@ -1 +0,0 @@ -7Vldb5swFP01kbaHSWAIIY8LbVdNm/ZQaZv25oABdwZTY/KxX79rbJLQhjTZmpKoSVBiHxt/3XOObsLACbLFJ4GL9CuPCBsgK1oMnKsBQmPfhk8FLDXgWb4GEkEjDdlr4I7+IQa0DFrRiJStjpJzJmnRBkOe5ySULQwLweftbjFn7VkLnJAnwF2I2VP0B41kqlF/aK3xW0KTtJnZtkxLhpvOZogyxRGfa6ju41wPnEBwLnUpWwSEqbNrzkUPdNPRulqYILnc5wbv+9Rb5veLb78ePpflz+JPtEQfhmZtctlsmESwf1PNeQ5fE8GrPCJqGAtqXMiUJzzH7AvnBYA2gPdEyqWJHq4kByiVGTOtMWUs4IyLegonHqo34KUU/DfZaPHq16qlOXE4q4leqFpd5/6bg+aVCMmOTTc8wiIhckc/ZxUlYDfhGZFiCfcJwrCks/Y6sOFZsuq3DgUUTDQOiIwZd4ZZZWYaII9JdTYFzlsx8x4qRaJJqA/yo1pjMn0HdIALZrc2Su/X3aGU6G8XLjXZYBioSxdGE1hCAHDg1NUr3a9ZBWxKL6QZ5DGPGAONKvrMUyrJXYHrmMzBJdrkwGWhhRvThSLZP7Al5rncwPUL8IIICtEgQs1O88RQqYtcMyIkWeym11M6mBtco3vjc6jxgfmGaxgo3TCMBntx/rhvUdnoHJSNtihbKcvuUmBLfY+C+p/iiobEj9xtAfPR1KkD1qOI3J415LyyBzt7MeDiv9uoszJcwx0b9Uwe2+nDgfcQ9HFzK3dPC7ZHvWZX7iW9Oit5O+jE8iu7l59OvcvbOw95e5cca18l9Z1kaaacaZZ1MDXASm9wRpk6/FvCZkTSEJsGYwK2CjRmNMmhEkJUiThQ8F0E7KDZSyTu/qllX34f9hzHxAvDbdGKRuOpZR3dnsd72jNCvdrz+JJ9nVX25Vknln019vK25L16AnLa8m4GvmRfzyup7+wLbfs38sjZ1yFOK1KeTavyead9wcxqh5i7DOF4mdXIe7XMCqrrx5F128YzXef6Lw== \ No newline at end of file diff --git a/docs/src/figures/value-node.svg b/docs/src/figures/value-node.svg deleted file mode 100644 index b5405551..00000000 --- a/docs/src/figures/value-node.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
2 \\ \{1,2,3\...
1 \\ \{1,2\}
3
Viewer does not support full SVG 1.1
\ No newline at end of file From 85ca4e3a4e6cbda53a61d7837b2f89d9519f35a1 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 31 Aug 2021 17:20:29 +0300 Subject: [PATCH 091/133] Minor changes to examples' documentations. --- docs/src/examples/CHD_preventative_care.md | 4 ++-- docs/src/examples/pig-breeding.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/examples/CHD_preventative_care.md b/docs/src/examples/CHD_preventative_care.md index cd661c27..9d66ff6c 100644 --- a/docs/src/examples/CHD_preventative_care.md +++ b/docs/src/examples/CHD_preventative_care.md @@ -66,7 +66,7 @@ add_node!(diagram, DecisionNode("T2", ["R1"], T_states)) add_node!(diagram, DecisionNode("TD", ["R2"], TD_states)) ``` -The value nodes are added in a similar fashion. However, value nodes do not have states because their purpose is to map their information states to utility values. Thus, value nodes are only given a name and their information set. +The value nodes are added in a similar fashion. However, value nodes do not have states because they map their information states to utility values instead of states. ```julia add_node!(diagram, ValueNode("TC", ["T1", "T2"])) @@ -120,7 +120,7 @@ $$\textit{risk estimate} = P(\text{CHD} \mid \text{test result}) = \frac{P(\text The probabilities $P(\text{test result} \mid \text{CHD})$ are test specific and these are read from the CSV data file. The updated risk estimates are aggregated according to the risk levels. These aggregated probabilities are then the state probabilities of node $R1$. The aggregating is done using function ```state_probabilities```. -In Decision Programming the probability distribution over the states of node $R1$ is defined into a $(101,2,3,101)$ probability matrix. This is because its information set consists of ($R0, H, T$) which have 101, 2 and 3 states respectively and the node $R1$ itself has 101 states. Here, one must know that in Decision Programming the states of the nodes are mapped to indices in the back-end. For instance, the health states $\text{CHD}$ and $\text{no CHD}$ are indexed 1 and 2. The testing decision states TRS,n GRS and no test are 1, 2 and 3. The order of the states is determined by the order in which they are defined when adding the nodes. Knowing this, we can declare the probability values straight into the probability matrix using a very compact syntax. Notice that we add 101 probability values at a time into the matrix. +In Decision Programming the probability distribution over the states of node $R1$ is defined into a $(101,2,3,101)$ probability matrix. This is because its information set consists of ($R0, H, T$) which have 101, 2 and 3 states respectively and the node $R1$ itself has 101 states. Here, one must know that in Decision Programming the states of the nodes are mapped to numbers in the back-end. For instance, the health states $\text{CHD}$ and $\text{no CHD}$ are indexed 1 and 2. The testing decision states TRS, GRS and no test are 1, 2 and 3. The order of the states is determined by the order in which they are defined when adding the nodes. Knowing this, we can declare the probability values straight into the probability matrix using a very compact syntax. Notice that we add 101 probability values at a time into the matrix. ```julia X_R = ProbabilityMatrix(diagram, "R1") diff --git a/docs/src/examples/pig-breeding.md b/docs/src/examples/pig-breeding.md index d1c5977c..8542c059 100644 --- a/docs/src/examples/pig-breeding.md +++ b/docs/src/examples/pig-breeding.md @@ -165,7 +165,7 @@ generate_diagram!(diagram, positive_path_utility = true) ## Decision Model -Next we initialise the JuMP model and add the decision variables. Then we addd the path compatibility variables. Since we applied an affine transformation to the utility function, making all path utilities positive, the probability cut can be excluded from the model. The purpose of this is discussed in the [theoretical section](../decision-programming/decision-model.md) of this documentation. +Next we initialise the JuMP model and add the decision variables. Then we add the path compatibility variables. Since we applied an affine transformation to the utility function, making all path utilities positive, the probability cut can be excluded from the model. The purpose of this is discussed in the [theoretical section](../decision-programming/decision-model.md) of this documentation. ```julia model = Model() From 5ebc9aa6297da57cc3057990ce49f6fb8a2dea47 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Fri, 3 Sep 2021 12:03:41 +0300 Subject: [PATCH 092/133] Proof read fixes to Usage.md. --- docs/src/usage.md | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/docs/src/usage.md b/docs/src/usage.md index 6113d79c..46d84713 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -9,21 +9,26 @@ using DecisionProgramming ## Adding nodes ![](figures/2chance_1decision_1value.svg) -Given the above influence diagram, we can express is as a Decision Programming model as follows. We create `ChanceNode` and `DecisionNode` instances and add them to the influence diagram. Creating a `ChanceNode` or `DecisionNode` requires us to give it a unique name, its information set and its states. If the node is a root node, the information set is left empty using square brackets. The order in which nodes are added does not matter. +Given the above influence diagram, we express it as a Decision Programming model as follows. We create `ChanceNode` and `DecisionNode` instances and add them to the influence diagram. Creating a `ChanceNode` or `DecisionNode` requires giving it a unique name, its information set and its states. If the node is a root node, the information set is left empty using square brackets. The order in which nodes are added does not matter. ```julia diagram = InfluenceDiagram() add_node!(diagram, DecisionNode("D1", [], ["a", "b"])) add_node!(diagram, ChanceNode("C2", ["D1", "C1"], ["v", "w"])) add_node!(diagram, ChanceNode("C1", [], ["x", "y", "z"])) +``` + +Value nodes are added by simply giving it a name and its information set. Value nodes do not have states because their purpose is to map their information state to utility values. + +```julia add_node!(diagram, ValueNode("V", ["C2"])) ``` -Once all the nodes are added, we generate the arcs. This function creates the abstraction of the influence diagram where each node is numbered, so that their predecessors have numbers less than theirs. In effect, the chance and decision nodes are numbered such that $C \cup D = \{1,...,n\}$, where $n = \mid C\mid + \mid D\mid$. The value nodes are numbered $V = \{n+1,..., N\}$, where $N = \mid C\mid + \mid D\mid + \mid V \mid$. For more details on influence diagrams see page [influence diagram](decision-programming/influence-diagram.md). +Once all the nodes are added, we generate the arcs. This orders the nodes and gives each one a number, so that their predecessors have numbers less than theirs. In effect, the chance and decision nodes are numbered such that $C \cup D = \{1,...,n\}$, where $n = \mid C\mid + \mid D\mid$. The value nodes are numbered $V = \{n+1,..., N\}$, where $N = \mid C\mid + \mid D\mid + \mid V \mid$. For more details on influence diagrams see page [influence diagram](decision-programming/influence-diagram.md). ```julia generate_arcs!(diagram) ``` -Now we can see that the influence diagram structures `Names`, `I_j`, `States`, `S`, `C`, `D` and `V` fields have been properly filled. Field Names holds all the names of nodes in the order of their numbers, from this we can see that node D1 has been number 1, node C1 number 2 and node C2 number 3. Field I_j holds the information sets of each node, and the nodes are identified by their numbers. Field States holds the names of the states of each node and field S holds the number of states each node has. Fields C, D and V hold the number of chance, decision and value nodes respectively. +Now the fields `Names`, `I_j`, `States`, `S`, `C`, `D` and `V` in the influence diagram structure have been properly filled. The `Names` field holds the names of all nodes in the order of their numbers. From this we can see that node D1 has been numbered 1, node C1 has been numbered 2 and node C2 has been numbered 3. Field `I_j` holds the information sets of each node. Notice, that the nodes are identified by their numbers. Field `States` holds the names of the states of each node and field `S` holds the number of states each node has. Fields `C`, `D` and `V` contain the chance, decision and value nodes respectively. ```julia julia> diagram.Names @@ -70,13 +75,13 @@ julia> diagram.V Each chance node needs a probability matrix which describes the probability distribution over its states given an information state. It holds probability values $$ℙ(X_j=s_j∣X_{I(j)}=𝐬_{I(j)})$$ -for all $s_j \in S_j$ and $s_{I(j)} \in S_{I(j)}$. +for all $s_j \in S_j$ and $𝐬_{I(j)} \in 𝐒_{I(j)}$. Thus, the probability matrix of a chance node needs to have dimensions that correspond to the number of states of the nodes in its information set and number of state of the node itself. -For example, the node C1 in the influence diagram above has an empty information set and it has three states x, y, and z. Therefore its probability matrix needs dimensions (3,1). If the probabilities of x, y and z happening are $10\%, 30\%$ and $60\%$ then the probability matrix $X_{C1}$ should be $[0.1 \quad 0.3 \quad 0.6]$. The order of the probabilities is determined by the order in which the states were declared when adding the node. +For example, the node C1 in the influence diagram above has an empty information set and three states $x, y$, and $z$. Therefore its probability matrix needs dimensions (3,1). If the probabilities of events $x, y$, and $z$ occuring are $10\%, 30\%$ and $60\%$, then the probability matrix $X_{C1}$ should be $[0.1 \quad 0.3 \quad 0.6]$. The order of the probability values is determined by the order in which the states are given when the node is added. The states are also stored in this order in the `States` vector. -In Decision Programming the probability matrix is added in the following way. Note, that probability matrices can only be added after the arcs have been generated. +In Decision Programming the probability matrix of node C1 can be added in the following way. Note, that probability matrices can only be added after the arcs have been generated. ```julia # How C1 was added: add_node!(diagram, ChanceNode("C1", [], ["x", "y", "z"])) @@ -92,7 +97,7 @@ julia> diagram.X ``` -As another example, we can look at the probability matrix of node C2 from the influence diagram above. It has two nodes in its information set: C1 and D1. These nodes have 3 and 2 state respectively. Node C2 itself has 2 states. The dimensions of the probability matrix should alway be $(\bold{S}_{I(j)}, S_j)$. The question however should it be (3, 2, 2) or (2, 3, 2)? The dimensions corresponding to the nodes in information set, should be in ascending order of the nodes' numbers. This is also the order in which they are in `diagram.I_j`. In this case the influence diagram looks like this: +As another example, we will add the probability matrix of node C2. It has two nodes in its information set: C1 and D1. These nodes have 3 and 2 state respectively. Node C2 itself has 2 states. The question is should the dimensions of the probability matrix be $(|S_{C1}|, |\ S_{D1}|, |\ S_{C2}|) = (3, 2, 2)$ or $(|S_{D1}|, |\ S_{C1}|, \ |S_{C2}|) = (2, 3, 2)$? The answer is that the dimensions should be in ascending order of the nodes' numbers that they correspond to. This is also the order that the information set is in in the field `I_j`. In this case the influence diagram looks like this: ```julia julia> diagram.Names 4-element Array{String,1}: @@ -115,7 +120,7 @@ julia> diagram.Names 2 ``` -Therefore, the probability matrix of node C2 should have dimensions (2, 3, 2). The probability matrix can be added by initialising the matrix and then filling in its elements as shown below. +Therefore, the probability matrix of node C2 should have dimensions $(|S_{D1}|, |\ S_{C1}|, \ |S_{C2}|) = (2, 3, 2)$. The probability matrix can be added by declaring the matrix and then filling in the probability values as shown below. ```julia X_C2 = zeros(2, 3, 2) X_C2[1, 1, 1] = ... @@ -124,8 +129,7 @@ X_C2[1, 1, 2] = ... ⋮ add_probabilities!(diagram, "C2", X_C2) ``` -It is crucial to understand what the matrix indices represent. Similarly as with the nodes in diagram.Names, states of a node are numbered according to their position in diagram.States vector of the node in question. The position of an element in the matrix represents a subpath in the influence diagram. However, the states in the path are referred to with their numbers instead of with their names. The order of the states in diagram.States of each node is seen below. From this we see that for nodes D1, C1, C2 the subpath `(1,1,1)` is $(a, x, v)$ and subpath `(1, 3, 2)` is $(a, z, w)$. -Therefore, the probability value at `X_C2[1, 3, 2]` should be the probability of the scenario $(a, z, w)$ occuring. +In order to be able to fill in the probability values, it is crucial to understand what the matrix indices represent. The indices represent a subpath in the influence diagram. The states in the path are referred to with their numbers instead of with their names. The states of a node are numbered according to their positions in the vector of states in field `States`. The order of the states of each node is seen below. From this, we can deduce that for nodes D1, C1, C2 the subpath `(1,1,1)` corresponds to subpath $(a, x, v)$ and subpath `(1, 3, 2)` corresponds to subpath $(a, z, w)$. Therefore, the probability value at `X_C2[1, 3, 2]` should be the probability of the scenario $(a, z, w)$ occuring. ```julia julia> diagram.States 3-element Array{Array{String,1},1}: @@ -134,7 +138,7 @@ julia> diagram.States ["v", "w"] ``` ### Helper Syntax -Figuring out the dimensions of a probability matrix and adding the probability values into in the way that was described above is difficult. Therefore, we have implemented an easier syntax. +Figuring out the dimensions of a probability matrix and adding the probability values is difficult. Therefore, we have implemented an easier syntax. A probability matrix can be initialised with the correct dimensions using the `ProbabilityMatrix` function. It initiliases the probability matrix with zeros. ```julia @@ -151,11 +155,14 @@ julia> X_C2 = ProbabilityMatrix(diagram, "C2") julia> size(X_C2) (2, 3, 2) ``` -A matrix of type `ProbabilityMatrix` can be filled using the names of the states. Notice that if we use the `Colon` (`:`) to indicate several elements of the matrix, the probability values have to be again given in the order of the states in the vector `diagram.States`. +A matrix of type `ProbabilityMatrix` can be filled using the names of the states. The states must however be given in the correct order, according to the order of the nodes in the information set vector `I_j`. Notice that if we use the `Colon` (`:`) to indicate several elements of the matrix, the probability values have to be given in the correct order of the states in `States`. ```julia julia> set_probability!(X_C2, ["a", "z", "w"], 0.25) 0.25 +julia> set_probability!(X_C2, ["z", "a", "v"], 0.75) +ERROR: DomainError with Node D1 does not have a state called z.: + julia> set_probability!(X_C2, ["a", "z", "v"], 0.75) 0.75 @@ -165,7 +172,7 @@ julia> set_probability!(X_C2, ["a", "x", :], [0.3, 0.7]) 0.7 ``` -A matrix of type `ProbabilityMatrix` can also be filled using the matrix indices if that is more convient. This is the same as above, only using the number indices instead of the state names. +A matrix of type `ProbabilityMatrix` can also be filled using the matrix indices if that is more convient. The following achieves the same as what was done above. ```julia julia> X_C2[1, 3, 2] = 0.25 0.25 @@ -179,7 +186,7 @@ julia> X_C2[1, 1, :] = [0.3, 0.7] 0.7 ```` -The probability matrix X_C2 now has elements with probability values. +Now, the probability matrix X_C2 is partially filled. ```julia julia> X_C2 2×3×2 ProbabilityMatrix{3}: @@ -192,13 +199,13 @@ julia> X_C2 0.0 0.0 0.0 ``` -The probability matrix can be added to the diagram once the correct probability values have been added into it. The probability matrix of node C2 is added exactly like before, despite X_C2 now being a matrix of type `ProbabilityMatrix`. +The probability matrix can be added to the influence diagram once it has been filled with probability values. The probability matrix of node C2 is added exactly like before, despite X_C2 now being a matrix of type `ProbabilityMatrix`. ```julia julia> add_probabilities!(diagram, "C2", X_C2) ``` ## Utility Matrices -Each value node maps its information states to utility values. In Decision Programming the utility values are passed to the influence diagram using utility matrices. Utility matrices are very similar to probability matrices of chance nodes. There are only two important differences. First, the utility matrices hold utility values instead of probabilities, meaning that they do not need to sum to one. Second, since value nodes do not have states, the utility matrix cardinality only depends on the number of states of the nodes in the information set have. +Each value node maps its information states to utility values. In Decision Programming the utility values are passed to the influence diagram using utility matrices. Utility matrices are very similar to probability matrices of chance nodes. There are only two important differences. First, the utility matrices hold utility values instead of probabilities, meaning that they do not need to sum to one. Second, since value nodes do not have states, the cardinality of a utility matrix depends only on the number of states of the nodes in the information set. As an example, the utility matrix of node V should have dimensions (2,1) because its information set consists of node C2, which has two states. If state $v$ of node C2 yields a utility of -100 and state $w$ yields utility of 400, then the utility matrix of node V can be added in the following way. Note, that utility matrices can only be added after the arcs have been generated. @@ -249,10 +256,9 @@ The final part of modeling an influence diagram using the Decision Programming p ```julia generate_diagram!(diagram) ``` -In this function, first, the probability and utility matrices are sorted according to the chance and value nodes' indices into the influence diagram's `X` and `Y` fields respectively. - -Second, the path probability and path utility types are set. These types define how the path probability $p(𝐬)$ and $\mathcal{U}(𝐬)$ are defined in the model. By default, the default path probability and default path utility are used. See the [influence diagram](decision-programming/influence-diagram.md) for more information on these. +In this function, first, the probability and utility matrices in fields `X` and `Y` are sorted according to the chance and value nodes' indices. +Second, the path probability and path utility types are declared and added into fields `P` and `U` respectively. These types define how the path probability $p(𝐬)$ and path utility $\mathcal{U}(𝐬)$ are defined in the model. By default, the function will set them to default path probability and default path utility. See the [influence diagram](decision-programming/influence-diagram.md) for more information on default path probability and utility. From 2c324876b397ed534e1734f9c395e6cc87482120 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Mon, 6 Sep 2021 09:22:32 +0300 Subject: [PATCH 093/133] Proof reading edits. --- docs/src/examples/CHD_preventative_care.md | 38 +++++------ docs/src/examples/n-monitoring.md | 26 ++++---- docs/src/examples/pig-breeding.md | 73 +++++++++++----------- docs/src/examples/used-car-buyer.md | 40 ++++++------ 4 files changed, 89 insertions(+), 88 deletions(-) diff --git a/docs/src/examples/CHD_preventative_care.md b/docs/src/examples/CHD_preventative_care.md index 9d66ff6c..dc6d7509 100644 --- a/docs/src/examples/CHD_preventative_care.md +++ b/docs/src/examples/CHD_preventative_care.md @@ -6,12 +6,12 @@ In this example, we will showcase the subproblem, which optimises the decision strategy given a single prior risk level. The chosen risk level in this example is 12%. The solution to the main problem is found in [^1]. -## Influence Diagram +## Influence diagram ![](figures/CHD_preventative_care.svg) The influence diagram representation of the problem is seen above. The chance nodes $R$ represent the patient's risk estimate – the prior risk estimate being $R0$. The risk estimate nodes $R0$, $R1$ and $R2$ have 101 states $R = \{0\%, 1\%, ..., 100\%\}$, which are the discretised risk levels for the risk estimates. -The risk estimate is updated according to the first and second test decisions, which are represented by decision nodes $T1$ and $T2$. These nodes have states $T = \{\text{TRS, GRS, no test}\}$. The health of the patient represented by chance node $H$ also affects the update of the risk estimate. In this model, the health of the patient indicates whether they will have a CHD event in the next ten years or not. Thus, the node has states $H = \{\text{CHD, no CHD}\}$. The treatment decision is represented by node $TD$ and it has states $TD = \{\text{treatment, no treatment}\}$. +The risk estimate is updated according to the first and second testing decisions, which are represented by decision nodes $T1$ and $T2$. These nodes have states $T = \{\text{TRS, GRS, no test}\}$. The health of the patient, represented by chance node $H$, also affects the update of the risk estimate. In this model, the health of the patient indicates whether they will have a CHD event in the next ten years or not. Thus, the node has states $H = \{\text{CHD, no CHD}\}$. The treatment decision is represented by node $TD$ and it has states $TD = \{\text{treatment, no treatment}\}$. The prior risk estimate represented by node $R0$ influences the health node $H$, because in the model we make the assumption that the prior risk estimate accurately describes the probability of having a CHD event. @@ -38,14 +38,14 @@ end ``` ### Initialise influence diagram -We start defining the decision programming model by initialising the influence diagram. +We start defining the Decision Programming model by initialising the influence diagram. ```julia diagram = InfluenceDiagram() ``` -For brevity in the following, we define the states of the nodes to be readily available. Notice, that $R_{states}$ is a vector with values $0\%, 1\%,..., 100\%$. +For brevity in the next sections, we define the states of the nodes to be readily available. Notice, that $R_{states}$ is a vector with values $0\%, 1\%,..., 100\%$. ```julia const H_states = ["CHD", "no CHD"] @@ -54,7 +54,7 @@ const TD_states = ["treatment", "no treatment"] const R_states = [string(x) * "%" for x in [0:1:100;]] ``` - Then we add the nodes. The chance and decision nodes are identified by their names. When declaring the nodes, they are also given information sets and states. Notice that nodes $R0$ and $H$ are root nodes, meaning that their information sets are empty. In Decision Programming, we add the chance and decision nodes in the follwoing way. + We then add the nodes. The chance and decision nodes are identified by their names. When declaring the nodes, they are also given information sets and states. Notice that nodes $R0$ and $H$ are root nodes, meaning that their information sets are empty. In Decision Programming, we add the chance and decision nodes in the follwoing way. ```julia add_node!(diagram, ChanceNode("R0", [], R_states)) add_node!(diagram, ChanceNode("R1", ["R0", "H", "T1"], R_states)) @@ -66,7 +66,7 @@ add_node!(diagram, DecisionNode("T2", ["R1"], T_states)) add_node!(diagram, DecisionNode("TD", ["R2"], TD_states)) ``` -The value nodes are added in a similar fashion. However, value nodes do not have states because they map their information states to utility values instead of states. +The value nodes are added in a similar fashion. However, value nodes do not have states because they map their information states to utility values instead. ```julia add_node!(diagram, ValueNode("TC", ["T1", "T2"])) @@ -81,7 +81,7 @@ generate_arcs!(diagram) ### Probabilities of the prior risk estimate and health of the patient -In this subproblem, the prior risk estimate is given and therefore the node $R0$ is in effect a deterministic node. In decision programming a deterministic node is added as a chance node for which the probability of one state is set to one and the probabilities of the rest of the states are set to zero. In this case +In this subproblem, the prior risk estimate is given and therefore the node $R0$ is in effect a deterministic node. In Decision Programming a deterministic node is added as a chance node for which the probability of one state is set to one and the probabilities of the rest of the states are set to zero. In this case $$ℙ(R0 = 12\%)=1$$ and @@ -104,7 +104,7 @@ $$ℙ(H = \text{no CHD} | R0 = \alpha) = 1 - \alpha$$ Since node $R0$ is deterministic and the health node $H$ is defined in this way, in our model the patient has a 12% chance of experiencing a CHD event and 88% chance of remaining healthy. -In Decision Programming the probability matrix of node $H$ has dimensions (101, 2) because its information set consisting of node $R0$ has 101 states and node $H$ has 2 states. We first set the column related to the state $CHD$ with values from `data.risk_levels` which are $0.00, 0.01, ..., 0.99, 1.00$ and the other column as it s complement event. +In Decision Programming the probability matrix of node $H$ has dimensions (101, 2) because its information set consisting of node $R0$ has 101 states and node $H$ has 2 states. We first set the column related to the state $CHD$ with values from `data.risk_levels` which are $0.00, 0.01, ..., 0.99, 1.00$ and the other column as its complement event. ```julia X_H = ProbabilityMatrix(diagram, "H") set_probability!(X_H, [:, "CHD"], data.risk_levels) @@ -114,13 +114,13 @@ add_probabilities!(diagram, "H", X_H) ### Probabilities of the updated the risk estimates -For node $R1%$, the probabilities of the states are calculated by aggregating the updated risk estimates, after a test is performed, into the risk levels. The updated risk estimates are calculated using the function ```update_risk_distribution```, which calculates the posterior probability distribution for a given health state, test and prior risk estimate. +For node $R1%$, the probabilities of the states are calculated by aggregating the updated risk estimates into the risk levels after a test is performed. The updated risk estimates are calculated using the function ```update_risk_distribution```, which calculates the posterior probability distribution for a given health state, test and prior risk estimate. $$\textit{risk estimate} = P(\text{CHD} \mid \text{test result}) = \frac{P(\text{test result} \mid \text{CHD})P(\text{CHD})}{P(\text{test result})}$$ The probabilities $P(\text{test result} \mid \text{CHD})$ are test specific and these are read from the CSV data file. The updated risk estimates are aggregated according to the risk levels. These aggregated probabilities are then the state probabilities of node $R1$. The aggregating is done using function ```state_probabilities```. -In Decision Programming the probability distribution over the states of node $R1$ is defined into a $(101,2,3,101)$ probability matrix. This is because its information set consists of ($R0, H, T$) which have 101, 2 and 3 states respectively and the node $R1$ itself has 101 states. Here, one must know that in Decision Programming the states of the nodes are mapped to numbers in the back-end. For instance, the health states $\text{CHD}$ and $\text{no CHD}$ are indexed 1 and 2. The testing decision states TRS, GRS and no test are 1, 2 and 3. The order of the states is determined by the order in which they are defined when adding the nodes. Knowing this, we can declare the probability values straight into the probability matrix using a very compact syntax. Notice that we add 101 probability values at a time into the matrix. +In Decision Programming the probability distribution over the states of node $R1$ is defined into a probability matrix with dimensions $(101,2,3,101)$. This is because its information set consists of nodes $R0, H$ and, $T$ which have 101, 2 and 3 states respectively and the node $R1$ itself has 101 states. Here, one must know that in Decision Programming the states of the nodes are mapped to numbers in the back-end. For instance, the health states $\text{CHD}$ and $\text{no CHD}$ are indexed 1 and 2. The testing decision states TRS, GRS and no test are 1, 2 and 3. The order of the states is determined by the order in which they are defined when adding the nodes. Knowing this, we can set the probability values into the probability matrix using a very compact syntax. Notice that we add 101 probability values at a time into the matrix. ```julia X_R = ProbabilityMatrix(diagram, "R1") @@ -135,7 +135,7 @@ We notice that the probability distrubtion is identical in $R1$ and $R2$ because add_probabilities!(diagram, "R2", X_R) ``` -### Utilities of test costs and health benefits +### Utilities of testing costs and health benefits We define a utility matrix for node $TC$, which maps all its information states to testing costs. The unit in which the testing costs are added is quality-adjusted life-year (QALYs). The utility matrix is defined and added in the following way. @@ -170,13 +170,13 @@ set_utility!(Y_HB, ["no CHD", "no treatment"], 7.70088349200034) add_utilities!(diagram, "HB", Y_HB) ``` -### Generate Influence Diagram +### Generate influence diagram Finally, we generate the full influence diagram before defining the decision model. By default this function uses the default path probabilities and utilities, which are defined as the joint probability of all chance events in the diagram and the sum of utilities in value nodes, respectively. In the [Contingent Portfolio Programming](contingent-portfolio-programming.md) example, we show how to use a user-defined custom path utility function. ## Decision Model -We define our JuMP model and declare the decision variables. +We define the JuMP model and declare the decision variables. ```julia model = Model() z = DecisionVariables(model, diagram) @@ -196,7 +196,7 @@ fixed_R0 = FixedPath(diagram, Dict("R0" => chosen_risk_level)) We also choose a scale factor of 10000, which will be used to scale the path probabilities. The probabilities need to be scaled because in this specific problem they are very small since the $R$ nodes have a large number of states. Scaling the probabilities helps the solver find an optimal solution. -We declare the path compatibility variables. We fix the state of the deterministic $R0$ node , forbid the unwanted testing strategies and scale the probabilities by giving them as parameters in the function call. +We then declare the path compatibility variables. We fix the state of the deterministic $R0$ node , forbid the unwanted testing strategies and scale the probabilities by giving them as parameters in the function call. ```julia scale_factor = 10000.0 @@ -225,8 +225,8 @@ optimize!(model) -## Analyzing Results -We obtain the results in the following way. +## Analyzing results +We extract the results in the following way. ```julia Z = DecisionStrategy(z) S_probabilities = StateProbabilities(diagram, Z) @@ -234,8 +234,8 @@ U_distribution = UtilityDistribution(diagram, Z) ``` -### Decision Strategy -We inspect the decision strategy. From the printout, we can see that when the prior risk level is 12% the optimal decision strategy is to first perform TRS testing. At the second decision stage, GRS should be conducted if the updated risk estimate is between 16% and 28% and otherwise no further testing should be conducted. Treatment should be provided to those who have a final risk estimate greater than 18%. Notice that the blank spaces in the table are states which have a probability of zero, which means that given this data it is impossible for the patient to have their risk estimate updated to those risk levels. +### Decision strategy +We inspect the decision strategy. From the printout, we can see that when the prior risk level is 12% the optimal decision strategy is to first perform TRS testing. At the second decision stage, GRS should be conducted if the updated risk estimate is between 16% and 28% and otherwise no further testing should be conducted. Treatment should be provided to those who have a final risk estimate greater than 18%. Notice that the incompatible states are not included in the printout. The incompatible states are those that have a state probability of zero, which means that given this data it is impossible for the patient to have their risk estimate updated to those risk levels. ```julia julia> print_decision_strategy(diagram, Z, S_probabilities) @@ -298,7 +298,7 @@ julia> print_decision_strategy(diagram, Z, S_probabilities) ``` -### Utility Distribution +### Utility distribution We can also print the utility distribution for the optimal strategy and some basic statistics for the distribution. diff --git a/docs/src/examples/n-monitoring.md b/docs/src/examples/n-monitoring.md index 1ecd6999..86d10bf3 100644 --- a/docs/src/examples/n-monitoring.md +++ b/docs/src/examples/n-monitoring.md @@ -27,21 +27,21 @@ const b = 0.03 fortification(k, a) = [c_k[k], 0][a] ``` -### Initialise Influence Diagram +### Initialising the influence diagram We initialise the influence diagram before adding nodes to it. ```julia diagram = InfluenceDiagram ``` -### Add Nodes +### Adding nodes Add node $L$ which represents the load on the structure. This node is the root node and thus, has an empty information set. Its states describe the state of the load, they are $high$ and $low$. ```julia add_node!(diagram, ChanceNode("L", [], ["high", "low"])) ``` -The report nodes $R_k$ and action nodes $A_k$ are easily added with a for-loop. The report nodes have node $L$ in their information sets and their states are $high$ and $low$. The actions are made based on these report, which is represented by the $R_k$ nodes forming the information sets of the $A_k$ nodes. The action nodes have states $yes$ and $no$, which represents decisions to either fortify the structure or not. +The report nodes $R_k$ and action nodes $A_k$ are easily added with a for-loop. The report nodes have node $L$ in their information sets and their states are $high$ and $low$. The actions are made based on these reports, which is represented by the action nodes $A_k$ having the report nodes $R_k$ in their information sets. The action nodes have states $yes$ and $no$, which represents decisions whether to fortify the structure or not. ```julia for i in 1:N @@ -60,7 +60,7 @@ The value node $T$ is added as follows. add_node!(diagram, ValueNode("T", ["F", ["A$i" for i in 1:N]...])) ``` -### Generate arcs +### Generating arcs Now that all of the nodes have been added to the influence diagram we generate the arcs between the nodes. This step automatically orders the nodes, gives them indices and reorganises the information into the appropriate form. ```julia generate_arcs!(diagram) @@ -87,7 +87,7 @@ $$ℙ(R_k=low∣L=low)=\max\{y,1-y\}$$ The probability of a correct report is thus in the range [0.5,1]. (This reflects the fact that a probability under 50% would not even make sense, since we would notice that if the test suggests a high load, the load is more likely to be low, resulting in that a low report "turns into" a high report and vice versa.) -In Decision Programming we add these probabilities in the following way. The probability matrix of an $R$ node is of dimensions (2,2), where the rows correspond to the states $high$ and $low$ of its predecessor node $L$ and the columns to its states $high$ and $low$. The probability matrix is declared and added in the following way. +In Decision Programming we add these probabilities by declaring probabilty matrices for nodes $R_k$. The probability matrix of a report node $R_k$ has dimensions (2,2), where the rows correspond to the states $high$ and $low$ of its predecessor node $L$ and the columns to its own states $high$ and $low$. ```julia for i in 1:N @@ -113,9 +113,9 @@ First we initialise the probability matrix for node $F$. X_F = ProbabilityMatrix(diagram, "F") ``` -This matrix has dimensions $(2, \textcolor{orange}{2, 2, 2, 2}, 2)$. This is because its information set consists of node $L$ and nodes $A_k$ all of which have 2 states and the node $F$ itself has 2 states. The orange colored dimensions correspond to the $A_k$ states, there are four of them because in this case $N = 4$. +This matrix has dimensions $(2, \textcolor{orange}{2, 2, 2, 2}, 2)$ because node $L$ and nodes $A_k$, which form the information set of $F$, all have 2 states and node $F$ itself also has 2 states. The orange colored dimensions correspond to the states of the action nodes $A_k$. -To set the probabilities we have to iterate over the information states. Here it helps to know that in Decision Programming the states of each node are mapped to numbers in the back-end. For instance, the load states $high$ and $low$ are referred to as 1 and 2. The same applies for the action states $yes$ and $no$, they are states 1 and 2. The `paths` function allows us to iterate over the paths of a specific nodes. In these paths, the states are refer to by their indices. Now we can iterate over the information states and set the probabilities into the probability matrix in the following way. +To set the probabilities we have to iterate over the information states. Here it helps to know that in Decision Programming the states of each node are mapped to numbers in the back-end. For instance, the load states $high$ and $low$ are referred to as 1 and 2. The same applies for the action states $yes$ and $no$, they are states 1 and 2. The `paths` function allows us to iterate over the subpaths of specific nodes. In these paths, the states are refer to by their indices. Using this information, we can easily iterate over the information states using the `paths` function and set the probability values into the probability matrix. ```julia @@ -129,7 +129,7 @@ for s in paths([State(2) for i in 1:N]) end ``` -We add the probability matrix to the influence diagram. +After declaring the probability matrix, we add it to the influence diagram. ```julia add_probabilities!(diagram, "F", X_F) ``` @@ -141,21 +141,21 @@ $$g(F=failure) = 0$$ $$g(F=success) = 100$$. -Utilities from the action states $A_k$ at target $T$ from are +Utilities from the action states $A_k$ at target $T$ are $$f(A_k=yes) = c_k$$ -$$f(A_k=no) = 0$$. +$$f(A_k=no) = 0.$$ The total cost is thus -$$Y(F, A_N, ..., A_1) = g(F) + (-f(A_N)) + ... + (-f(A_1))$$. +$$Y(F, A_N, ..., A_1) = g(F) + (-f(A_N)) + ... + (-f(A_1)).$$ We first declare the utility matrix for node $T$. ```julia Y_T = UtilityMatrix(diagram, "T") ``` -This matrix has dimensions $(2, \textcolor{orange}{2, 2, 2, 2})$, where again the twos correspond to the numbers of states the nodes in the information set have. Similarly as before, the first dimension corresponds to the states of node $F$ and the other 4 dimensions (in oragne) correspond to the states of the $A_k$ nodes. The utilities are set and added in the following way. +This matrix has dimensions $(2, \textcolor{orange}{2, 2, 2, 2})$, where the dimensions correspond to the numbers of states the nodes in the information set have. Similarly as before, the first dimension corresponds to the states of node $F$ and the other 4 dimensions (in oragne) correspond to the states of the $A_k$ nodes. The utilities are set and added similarly to how the probabilities were added above. ```julia for s in paths([State(2) for i in 1:N]) @@ -241,7 +241,7 @@ julia> print_decision_strategy(diagram, Z, S_probabilities) ``` -The state probabilities for the strategy $Z$ can also be obtained. These tell the probability of each state in each node, given the strategy $Z$. +The state probabilities for strategy $Z$ are also obtained. These tell the probability of each state in each node, given strategy $Z$. ```julia-repl julia> print_state_probabilities(sprobs, L) diff --git a/docs/src/examples/pig-breeding.md b/docs/src/examples/pig-breeding.md index 8542c059..29f8282c 100644 --- a/docs/src/examples/pig-breeding.md +++ b/docs/src/examples/pig-breeding.md @@ -2,45 +2,46 @@ ## Description The pig breeding problem as described in [^1]. -"A pig breeder is growing pigs for a period of four months and subsequently selling them. During this period the pig may or may not develop a certain disease. If the pig has the disease at the time it must be sold, the pig must be sold for slaughtering, and its expected market price is then 300 DKK (Danish kroner). If it is disease free, its expected market price as a breeding animal is 1000 DKK +>A pig breeder is growing pigs for a period of four months and subsequently selling them. During this period the pig may or may not develop a certain disease. If the pig has the disease at the time it must be sold, the pig must be sold for slaughtering, and its expected market price is then 300 DKK (Danish kroner). If it is disease free, its expected market price as a breeding animal is 1000 DKK +> +>Once a month, a veterinary doctor sees the pig and makes a test for presence of the disease. If the pig is ill, the test will indicate this with probability 0.80, and if the pig is healthy, the test will indicate this with probability 0.90. At each monthly visit, the doctor may or may not treat the pig for the disease by injecting a certain drug. The cost of an injection is 100 DKK. +> +>A pig has the disease in the first month with probability 0.10. A healthy pig develops the disease in the subsequent month with probability 0.20 without injection, whereas a healthy and treated pig develops the disease with probability 0.10, so the injection has some preventive effect. An untreated pig that is unhealthy will remain so in the subsequent month with probability 0.90, whereas the similar probability is 0.50 for an unhealthy pig that is treated. Thus spontaneous cure is possible, but treatment is beneficial on average. -Once a month, a veterinary doctor sees the pig and makes a test for presence of the disease. If the pig is ill, the test will indicate this with probability 0.80, and if the pig is healthy, the test will indicate this with probability 0.90. At each monthly visit, the doctor may or may not treat the pig for the disease by injecting a certain drug. The cost of an injection is 100 DKK. -A pig has the disease in the first month with probability 0.10. A healthy pig develops the disease in the subsequent month with probability 0.20 without injection, whereas a healthy and treated pig develops the disease with probability 0.10, so the injection has some preventive effect. An untreated pig that is unhealthy will remain so in the subsequent month with probability 0.90, whereas the similar probability is 0.50 for an unhealthy pig that is treated. Thus spontaneous cure is possible, but treatment is beneficial on average." - - -## Influence Diagram +## Influence diagram ![](figures/n-month-pig-breeding.svg) -The influence diagram for the the generalized $N$-month pig breeding. The nodes are associated with the following states. **Health states** $h_k=\{ill,healthy\}$ represent the health of the pig at month $k=1,...,N$. **Test states** $t_k=\{positive,negative\}$ represent the result from testing the pig at month $k=1,...,N-1$. **Treat states** $d_k=\{treat, pass\}$ represent the decision to treat the pig with an injection at month $k=1,...,N-1$. +The influence diagram for the generalized $N$-month pig breeding problem. The nodes are associated with the following states. **Health states** $h_k=\{ill,healthy\}$ represent the health of the pig at month $k=1,...,N$. **Test states** $t_k=\{positive,negative\}$ represent the result from testing the pig at month $k=1,...,N-1$. **Treatment states** $d_k=\{treat, pass\}$ represent the decision to treat the pig with an injection at month $k=1,...,N-1$. > The dashed arcs represent the no-forgetting principle and we can toggle them on and off in the formulation. -In decision programming, we start by initialising an empty influence diagram. For this problem, we declare $N = 4$ because we are solving the 4 month pig breeding problem in this example. +In this example, we solve the 4 month pig breeding problem and thus, declare $N = 4$. ```julia using JuMP, Gurobi using DecisionProgramming const N = 4 - +``` +In Decision Programming, we start by initialising an empty influence diagram. Then we define the nodes with their information sets and states and add them to the influence diagram. +```julia diagram = InfluenceDiagram() ``` -Next, we define the nodes with their information sets and states. We add the nodes to the influence diagram. -### Health at First Month +### Health at first month -As seen in the influence diagram, the node $h_1$ has no arcs into it, making it a root node. Therefore, the information set $I(h_1)$ is empty. The state of this node are $ill$ and $healthy$. +As seen in the influence diagram, the node $h_1$ has no arcs into it making it a root node. Therefore, the information set $I(h_1)$ is empty. The states of this node are $ill$ and $healthy$. ```julia add_node!(diagram, ChanceNode("H1", [], ["ill", "healthy"])) ``` -### Health, Test Results and Treatment Decisions at Subsequent Months -The chance and decision nodes representing the health, test results, treatment decisions for the following months can be added easily using a for-loop. The value node representing the testing costs in each month is also added. Each node is given a name, its information set and states. Notice, that value nodes do not have states because their purpose is to map their information state to utilities, which will be added later. Notice also, that here we do not assume the no-forgetting principle and thus the information set of the treatment decision is made only based on the previous test result. Remember, that the first health node $h_1$ was already added above. +### Health, test results and treatment decisions at subsequent months +The chance and decision nodes representing the health, test results, treatment decisions for the following months can be added easily using a for-loop. The value node representing the treatment costs in each month is also added. Each node is given a name, its information set and states. Remember that value nodes do not have states. Notice that we do not assume the no-forgetting principle and thus, the information sets of the treatment decisions only contain the previous test result. ```julia for i in 1:N-1 @@ -55,14 +56,14 @@ for i in 1:N-1 end ``` -### Market Price -The final value node represented the market price is added. It has the final health node $h_n$ as its information set. +### Market price +The final value node, representing the market price, is added. It has the final health node $h_N$ as its information set. ```julia add_node!(diagram, ValueNode("MP", ["H$N"])) ``` ### Generate arcs -Now that all of the nodes have been added to our influence diagram we generate the arcs between the nodes. This step automatically orders the nodes, gives them indices and reorganises the information into the appropriate form. +Now that all of the nodes have been added to the influence diagram, we generate the arcs between the nodes. This step automatically orders the nodes, gives them indices and reorganises the information into the correct form. ```julia generate_arcs!(diagram) ``` @@ -77,12 +78,12 @@ We obtain the complement probabilities for binary states by subtracting from one $$ℙ(h_1 = healthy)=1-ℙ(h_1 = ill).$$ -In decision programming, we add these probabilities for node $h_1$ as follows. Notice, that the probability vector is ordered according to the order of states when defining node $h_1$. +In Decision Programming, we add these probabilities for node $h_1$ as follows. Notice, that the probability vector is ordered according to the order that the states were given in when defining node $h_1$. More information on the syntax of adding probabilities is found on the [usage page](../usage.md). ```julia add_probabilities!(diagram, "H1", [0.1, 0.9]) ``` -The probability distributions for the other health nodes are identical, thus we define one probability matrix and use it for all the subsequent months' health nodes. The probability that the pig is ill in the subsequent months $k=2,...,N$ depends on the treatment decision and state of health in the previous month $k-1$. The nodes $h_{k-1}$ and $d_{k-1}$ are thus in the information set $I(h_k)$, meaning that the probability distribution of $h_k$ is conditional on these nodes: +The probability distributions for the other health nodes are identical. Thus, we define one probability matrix and use it for all the subsequent months' health nodes. The probability that the pig is ill in the subsequent months $k=2,...,N$ depends on the treatment decision and state of health in the previous month $k-1$. The nodes $h_{k-1}$ and $d_{k-1}$ are thus in the information set $I(h_k)$, meaning that the probability distribution of $h_k$ is conditional on these nodes: $$ℙ(h_k = ill ∣ h_{k-1} = healthy, \ d_{k-1} = pass)=0.2,$$ @@ -92,7 +93,7 @@ $$ℙ(h_k = ill ∣ h_{k-1} = ill, \ d_{k-1} = pass)=0.9,$$ $$ℙ(h_k = ill ∣ h_{k-1} = ill, \ d_{k-1} = treat)=0.5.$$ -The probability matrix is define in Decision Programming in the following way. Notice, that the ordering of the information state corresponds to the order in which the information set was defined when adding the health nodes. +In Decision Programming, the probability matrix is define in the following way. Notice, that the ordering of the information state corresponds to the order in which the information set was defined when adding the health nodes. ```julia X_H = ProbabilityMatrix(diagram, "H2") set_probability!(X_H, ["healthy", "pass", :], [0.2, 0.8]) @@ -101,13 +102,13 @@ set_probability!(X_H, ["ill", "pass", :], [0.9, 0.1]) set_probability!(X_H, ["ill", "treat", :], [0.5, 0.5]) ``` -Next we define the probability matrix for the test results. Here again, we note that the probability distributions for all test results are identical, and thus we only define the matrix once. For the probabilities that the test indicates a pig's health correctly at month $k=1,...,N-1$, we have +Next we define the probability matrix for the test results. Here again, we note that the probability distributions for all test results are identical, and thus we only define the probability matrix once. For the probabilities that the test indicates a pig's health correctly at month $k=1,...,N-1$, we have $$ℙ(t_k = positive ∣ h_k = ill) = 0.8,$$ $$ℙ(t_k = negative ∣ h_k = healthy) = 0.9.$$ -In decision programming: +In Decision Programming: ```julia X_T = ProbabilityMatrix(diagram, "T1") @@ -117,7 +118,7 @@ set_probability!(X_T, ["healthy", "negative"], 0.9) set_probability!(X_T, ["healthy", "positive"], 0.1) ``` -We add the probability matrices into the influence diagram as follows. +We add the probability matrices into the influence diagram using a for-loop. ```julia for i in 1:N-1 @@ -129,41 +130,41 @@ end ### Utilities -The cost of treatment decision for the pig at month $k=1,...,N-1$ is defined +The cost incurred by the treatment decision at month $k=1,...,N-1$ is $$Y(d_k=treat) = -100,$$ $$Y(d_k=pass) = 0.$$ -In decision programming the utility values are added as follows. Notice that the values in the utility matrix are ordered according to the order in which the information set was given when adding the node. +In Decision Programming the utility values are added using utility matrices. Notice that the utility values in the matrix are given in the same order as the states of node $h_N$ were defined when node $h_N$ was added. ```julia for i in 1:N-1 add_utilities!(diagram, "C$i", [-100.0, 0.0]) end ``` -The market price of given the pig health at month $N$ is defined +The market price of the pig given its health at month $N$ is $$Y(h_N=ill) = 300,$$ $$Y(h_N=healthy) = 1000.$$ -In decision programming: +In Decision Programming: ```julia add_utilities!(diagram, "MP", [300.0, 1000.0]) ``` -### Generate Influence Diagram -Finally, we generate the full influence diagram before defining the decision model. By default this function uses the default path probabilities and utilities, which are defined as the joint probability of all chance events in the diagram and the sum of utilities in value nodes, respectively. In the [Contingent Portfolio Programming](contingent-portfolio-programming.md) example, we show how to use a user-defined custom path utility function. +### Generate influence diagram +After adding nodes, generating arcs and defining probability and utility values, we generate the full influence diagram. By default this function uses the default path probabilities and utilities, which are defined as the joint probability of all chance events in the diagram and the sum of utilities in value nodes, respectively. In the [Contingent Portfolio Programming](contingent-portfolio-programming.md) example, we show how to use a user-defined custom path utility function. -In the pig breeding problem, when the $N$ is large some of the path utilities become negative. In this case, we choose to use the [positive path utility](../decision_model.md) transformation, which allows us to exclude the probability cut in the next section. +In the pig breeding problem, when the $N$ is large some of the path utilities become negative. In this case, we choose to use the [positive path utility](../decision-programming/decision_model.md) transformation, which allows us to exclude the probability cut in the next section. ```julia generate_diagram!(diagram, positive_path_utility = true) ``` -## Decision Model +## Decision model Next we initialise the JuMP model and add the decision variables. Then we add the path compatibility variables. Since we applied an affine transformation to the utility function, making all path utilities positive, the probability cut can be excluded from the model. The purpose of this is discussed in the [theoretical section](../decision-programming/decision-model.md) of this documentation. @@ -192,7 +193,7 @@ optimize!(model) ``` -## Analyzing Results +## Analyzing results Once the model is solved, we extract the results. The results are the decision strategy, state probabilities and utility distribution. @@ -202,7 +203,7 @@ S_probabilities = StateProbabilities(diagram, Z) U_distribution = UtilityDistribution(diagram, Z) ``` -### Decision Strategy +### Decision strategy The optimal decision strategy is: @@ -230,9 +231,9 @@ julia> print_decision_strategy(diagram, Z, S_probabilities) The optimal strategy is to not treat the pig in the first month regardless of if it is sick or not. In the two subsequent months, the pig should be treated if the test result is positive. -### State Probabilities +### State probabilities -The state probabilities for the strategy $Z$ tell the probability of each state in each node, given the strategy $Z$. +The state probabilities for strategy $Z$ tell the probability of each state in each node, given strategy $Z$. ```julia-repl @@ -272,7 +273,7 @@ julia> print_state_probabilities(diagram, S_probabilities, treatment_nodes) └────────┴──────────┴──────────┴─────────────┘ ``` -### Utility Distribution +### Utility distribution We can also print the utility distribution for the optimal strategy. The selling prices for a healthy and an ill pig are 1000DKK and 300DKK, respectively, while the cost of treatment is 100DKK. We can see that the probability of the pig being ill in the end is the sum of three first probabilities, approximately 30.5%. This matches the probability of state $ill$ in the last node $h_4$ in the state probabilities shown above. diff --git a/docs/src/examples/used-car-buyer.md b/docs/src/examples/used-car-buyer.md index 65833621..2880c7ec 100644 --- a/docs/src/examples/used-car-buyer.md +++ b/docs/src/examples/used-car-buyer.md @@ -24,7 +24,7 @@ using DecisionProgramming diagram = InfluenceDiagram() ``` -### Car's State +### Car's state The chance node $O$ is defined by its name, its information set $I(O)$ and its states $lemon$ and $peach$. As seen in the influence diagram, the information set is empty and the node is a root node. @@ -32,7 +32,7 @@ The chance node $O$ is defined by its name, its information set $I(O)$ and its s add_node!(diagram, ChanceNode("O", [], ["lemon", "peach"])) ``` -### Stranger's Offer Decision +### Stranger's offer decision A decision node is also defined by its name, its information set and its states. @@ -40,15 +40,15 @@ A decision node is also defined by its name, its information set and its states. add_node!(diagram, DecisionNode("T", [], ["no test", "test"])) ``` -### Test's Outcome +### Test's outcome -The second chance node, $R$, has nodes $O$ and $T$ in its information set, and three states describing the situations of no test being done, and the test declaring the car to be a lemon or a peach. +The second chance node $R$ has nodes $O$ and $T$ in its information set, and three states describing the situations of no test being done, and the test declaring the car to be a lemon or a peach. ```julia add_node!(diagram, ChanceNode("R", ["O", "T"], ["no test", "lemon", "peach"])) ``` -### Purchace Decision +### Purchace decision The purchase decision represented by node $A$ is added as follows. ```julia add_node!(diagram, DecisionNode("A", ["R"], ["buy without guarantee", "buy with guarantee", "don't buy"])) @@ -63,15 +63,15 @@ add_node!(diagram, ValueNode("V3", ["O", "A"])) ``` ### Generate arcs -Now that all of the nodes have been added to our influence diagram we generate the arcs between the nodes. This step automatically orders the nodes, gives them indices and reorganises the information into the appropriate form. +Now that all of the nodes have been added to our influence diagram we generate the arcs between the nodes. This step automatically orders the nodes, gives them indices and reorganises the information into the appropriate form in the influence diagram structure. ```julia generate_arcs!(diagram) ``` ### Probabilities -We continue by defining the probability distributions for each chance node. +We continue by defining probability distributions for each chance node. -Node $O$ is a root node and has two states thus, its probability distribution is simply defined over the two states. We can use the `ProbabilityMatrix` structure in creating the probability matrix easily without having to worry about the matrix dimentions. Then we add the probabilities to the influence diagram. +Node $O$ is a root node and has two states thus, its probability distribution is simply defined over the two states. We can use the `ProbabilityMatrix` structure in creating the probability matrix easily without having to worry about the matrix dimensions. We then set the probability values and add the probabililty matrix to the influence diagram. ```julia X_O = ProbabilityMatrix(diagram, "O") set_probability!(X_O, ["peach"], 0.8) @@ -79,7 +79,7 @@ set_probability!(X_O, ["lemon"], 0.2) add_probabilities!(diagram, "O", X_O) ``` -Node $R$ has two nodes in its information set and three states. the probabilities $P(s_j \mid s_{I(j)})$ must thus be defined for all combinations of states in $O$, $T$ and $R$. Since these nodes have 2, 3, and 3 states respectively, the probability matrix has 12 elements. We will set 3 probability values at a time to make this feat more swift. +Node $R$ has two nodes in its information set and three states. The probabilities $P(s_j \mid s_{I(j)})$ must thus be defined for all combinations of states in $O$, $T$ and $R$. We declare the probability distribution over the states of node $R$ for each information state in the following way. More information on defining probability matrices can be found on the [usage page](../usage.md). ```julia X_R = ProbabilityMatrix(diagram, "R") set_probability!(X_R, ["lemon", "no test", :], [1,0,0]) @@ -91,9 +91,9 @@ add_probabilities!(diagram, "R", X_R) ### Utilities -We continue by defining the utilities associated with value nodes. The utilities $Y_j(𝐬_{I(j)})$ are defined and added similarly to the probabilities. +We continue by defining the utilities associated with the information states of the value nodes. The utilities $Y_j(𝐬_{I(j)})$ are defined and added similarly to the probabilities. -Value node $V1$ has only node $T$ in its information set and node $T$ only has two states. Therefore, node $V1$ needs to map exactly two utility values, on for state $tes$ and the other for $no test$. +Value node $V1$ has only node $T$ in its information set and node $T$ only has two states. Therefore, the utility matrix of node $V1$ should hold utility values corresponding to states $test$ and $no \ test$. ```julia Y_V1 = UtilityMatrix(diagram, "V1") @@ -102,7 +102,7 @@ set_utility!(Y_V1, ["no test"], 0) add_utilities!(diagram, "V1", Y_V1) ``` -We then define the utilities describing the base profit of of the purchase. +We then define the utilities associated with the base profit of the purchase in different scenarios. ```julia Y_V2 = UtilityMatrix(diagram, "V2") set_utility!(Y_V2, ["buy without guarantee"], 100) @@ -111,7 +111,7 @@ set_utility!(Y_V2, ["don't buy"], 0) add_utilities!(diagram, "V2", Y_V2) ``` -Finally, we add the utilities corresponding to the repair costs. The rows of the utilities matrix `Y_V3` correspond to the state of the car, while the columns correspond to the decision made in node $A$. The utilities can be added as follows. Notice that the utility values for the second row are added in one line, in this case it is important to give the utility values in the right order. The order of the columns is determined by the order in which the states are given when declaring node $A$. See the [usage page](../usage.md) for more on this more compact syntax. +Finally, we define the utilities corresponding to the repair costs. The rows of the utilities matrix `Y_V3` correspond to the state of the car, while the columns correspond to the decision made in node $A$. Notice that the utility values for the second row are added as a vector, in this case it is important to give the utility values in the correct order. The order of the columns is determined by the order in which the states are given when declaring node $A$. See the [usage page](../usage.md) for more information on the syntax. ```julia Y_V3 = UtilityMatrix(diagram, "V3") set_utility!(Y_V3, ["lemon", "buy without guarantee"], -200) @@ -121,15 +121,15 @@ set_utility!(Y_V3, ["peach", :], [-40, -20, 0]) add_utilities!(diagram, "V3", Y_V3) ``` -### Generate Influence Diagram +### Generate influence diagram Finally, generate the full influence diagram before defining the decision model. By default this function uses the default path probabilities and utilities, which are defined as the joint probability of all chance events in the diagram and the sum of utilities in value nodes, respectively. In the [Contingent Portfolio Programming](contingent-portfolio-programming.md) example, we show how to use a user-defined custom path utility function. ```julia generate_diagram!(diagram) ``` -## Decision Model -We then construct the decision model using the DecisionProgramming.jl package, using the expected value as the objective. +## Decision model +We then construct the decision model by declaring a JuMP model and adding decision variables and path compatibility variables to the model. We define the objective function to be the expected value. ```julia model = Model() @@ -151,8 +151,8 @@ optimize!(model) ``` -## Analyzing Results -Once the model is solved, we extract the results. The results are the decision strategy, state probabilities and utility distribution. +## Analyzing results +Once the model is solved, we extract the results. ```julia Z = DecisionStrategy(z) @@ -160,7 +160,7 @@ S_probabilities = StateProbabilities(diagram, Z) U_distribution = UtilityDistribution(diagram, Z) ``` -### Decision Strategy +### Decision strategy We obtain the following optimal decision strategy: ```julia-repl @@ -178,7 +178,7 @@ julia> print_decision_strategy(diagram, Z, S_probabilities) └───────────────┴───────────────────────┘ ``` -### Utility Distribution +### Utility distribution ```julia-repl julia> print_utility_distribution(U_distribution) ┌───────────┬─────────────┐ From 4398583d72bd84ff91e17f7adb95044c7ac43284 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Mon, 6 Sep 2021 09:23:41 +0300 Subject: [PATCH 094/133] Edited docstring of printing function. --- src/printing.jl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/printing.jl b/src/printing.jl index 59b3c2ab..f28f8707 100644 --- a/src/printing.jl +++ b/src/printing.jl @@ -2,16 +2,22 @@ using DataFrames, PrettyTables using StatsBase, StatsBase.Statistics """ - print_decision_strategy(diagram::InfluenceDiagram, Z::DecisionStrategy, state_probabilities::StateProbabilities; show_incompatible_states = false) + print_decision_strategy(diagram::InfluenceDiagram, Z::DecisionStrategy, state_probabilities::StateProbabilities; show_incompatible_states::Bool = false) Print decision strategy. +# Arguments +- `diagram::InfluenceDiagram`: Influence diagram structure. +- `Z::DecisionStrategy`: Decision strategy structure with optimal decision strategy. +- `state_probabilities::StateProbabilities`: State probabilities structure corresponding to optimal decision strategy. +- `show_incompatible_states::Bool`: Choice to print rows also for incompatible states. + # Examples ```julia >julia print_decision_strategy(diagram, Z, S_probabilities) ``` """ -function print_decision_strategy(diagram::InfluenceDiagram, Z::DecisionStrategy, state_probabilities::StateProbabilities; show_incompatible_states = false) +function print_decision_strategy(diagram::InfluenceDiagram, Z::DecisionStrategy, state_probabilities::StateProbabilities; show_incompatible_states::Bool = false) probs = state_probabilities.probs for (d, I_d, Z_d) in zip(Z.D, Z.I_d, Z.Z_d) From 8ca9adb96ce693d59712209f39062cc46b746a1b Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Mon, 6 Sep 2021 13:10:31 +0300 Subject: [PATCH 095/133] Updated the read me file. --- README.md | 34 ++++++++++++++++++------------- examples/figures/simple-id.drawio | 2 +- examples/figures/simple-id.svg | 2 +- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index f938ca4f..c084ad92 100644 --- a/README.md +++ b/README.md @@ -14,16 +14,22 @@ We can create an influence diagram as follows: ```julia using DecisionProgramming -S = States([2, 2, 2, 2]) -C = [ChanceNode(2, [1]), ChanceNode(3, [1])] -D = [DecisionNode(1, Node[]), DecisionNode(4, [2, 3])] -V = [ValueNode(5, [4])] -X = [Probabilities(2, [0.4 0.6; 0.6 0.4]), Probabilities(3, [0.7 0.3; 0.3 0.7])] -Y = [Consequences(5, [1.5, 1.7])] -validate_influence_diagram(S, C, D, V) -sort!.((C, D, V, X, Y), by = x -> x.j) -P = DefaultPathProbability(C, X) -U = DefaultPathUtility(V, Y) + +diagram = InfluenceDiagram() + +add_node!(diagram, DecisionNode("A", [], ["a", "b"])) +add_node!(diagram, DecisionNode("D", ["B", "C"], ["k", "l"])) +add_node!(diagram, ChanceNode("B", ["A"], ["x", "y"])) +add_node!(diagram, ChanceNode("C", ["A"], ["v", "w"])) +add_node!(diagram, ValueNode("V", ["D"])) + +generate_arcs!(diagram) + +add_probabilities!(diagram, "B", [0.4 0.6; 0.6 0.4]) +add_probabilities!(diagram, "C", [0.7 0.3; 0.3 0.7]) +add_utilities!(diagram, "V", [1.5, 1.7]) + +generate_diagram!(diagram) ``` Using the influence diagram, we create the decision model as follow: @@ -31,13 +37,13 @@ Using the influence diagram, we create the decision model as follow: ```julia using JuMP model = Model() -z = DecisionVariables(model, S, D) -x_s = PathCompatibilityVariables(model, z, S, P) -EV = expected_value(model, x_s, U, P) +z = DecisionVariables(model, diagram) +x_s = PathCompatibilityVariables(model, diagram, z) +EV = expected_value(model, diagram, x_s) @objective(model, Max, EV) ``` -Finally, we can optimize the model using MILP solver. +We can optimize the model using MILP solver. ```julia using Gurobi diff --git a/examples/figures/simple-id.drawio b/examples/figures/simple-id.drawio index f824a7f8..3303c031 100644 --- a/examples/figures/simple-id.drawio +++ b/examples/figures/simple-id.drawio @@ -1 +1 @@ -7Vhdb5swFP01SNvDJDDYyR6XtFsndQ9THvbsgPnoDKaOKUl//WxsEgghoWoEmTaCgn18ba7v8bm2sNxluv3GcR7/YAGhFrCDreXeWQCg2Uz+K2CnAQ/YGoh4EmjIOQCr5JUYsDYrkoBsWoaCMSqSvA36LMuIL1oY5pyVbbOQ0fZbcxyRDrDyMe2iv5JAxBqdQ/uAP5Akius3O7ZpSXFtbIbYxDhgpYYqG/fecpecMaFL6XZJqIpdHRc90Nee1r1jnGRiSIfd489X9FwG5BU+wALk4ep7/Omz8U3s6gmTQM7fVDOWyceCsyILiBrGljXGRcwilmH6yFguQUeCT0SInWEPF4JJKBYpNa0bwdnvffzkzBdhQumSUcarl7oBJPPA21s2WuZg7SIkW7Sjyrve+deBZgX3yZlJ1+sI84iIM3Zwz5Jc3YSlRPCd7McJxSJ5afuBzTqL9nYHKmTBsPEGZsy4L5gW5k0W8OStQAsu1a0Ls4U0VRVQ1e60WZdXSqVmFJ1lnAiyynEVo1Kqtk0W3uRaSGGyVaQfcxVC9TvFFaou1YNlooHr6+Q6yAlPZLwIV/4kWWRgM3fCBdmep7tLj+ngIiPEOvF4pl42ZGyguKHgGrs6obMOJe/W1duZ6TJwRV2BgbrqIW4cXc1vI+MN4GoCZrwpmQEnMh6iQsUox1mLM/RcqG1z4euwfVE+RusP0FE5sUqGreLHQw9ZivRTZUlnaDLVfshpaVfqMY5W0jsz64BdcMwMCrwby6Buz5YIe7Y8HrN0XWwub3nH2gxD4PuneAjQGsGxeUD2jfFQn82nzqHjnhq9v+HU6F3Ioc3k5143+f3rB0yIppal0+VkkqPN5fR5RVnCgbJ0p5QlHP9o4/0/2vRr15uPtqXK6uEbT9XW+FDm3v8B \ No newline at end of file +5Vhbb5swFP41SNvDJG4m2WNDunZS9zBF2p4NmMtqMHVMgPz62dgkEHJruiZMJZGwv3N8O5/PZwvNctPqgcI8/kEChDVTDyrNmmumaYCJyV8CqRViGAqJaBIobAsskjVSoK7QIgnQsufICMEsyfugT7IM+ayHQUpJ2XcLCe6PmsMIDYCFD/EQ/Z0ELJboFOhb/BElUdyObOjKksLWWXWxjGFASgk1Pta9ZrmUECZLaeUiLKLXxkV29O2AdTMxijJ2ToP66efaeSkDtAaPoDDzcPE9/vJVzY3V7YJRwNevqhnJ+GtGSZEFSHSj8xqhLCYRySB+IiTnoMHBP4ixWrEHC0Y4FLMUK+uSUfK8iR9f+SxMMHYJJrQZ1AoAmgb2xrNjmZqe5TjcMlxuG1dSUB8dWWO7bSCNEDviB6SfCEBnABXMB0RSxGjNHSjCkCWr/gaBap9FG78tFbyg2HgFM6rfFcSFGkkzbf7ngdA14Iq/LExmlWaKSt3U5tJtyCvGPGcEnWWcMLTIYRO0kudtnyy4zGUihUklSN/lKgTit48rp3lEC5KxDi6fvfsgRzTh8UJUzCfJIgUfJHuFKEPVUXqU1XJUIirlsW1VLztprKC4k8Et9s8JnQwoeXNevZ6ZIQOX55V5Zl4Zo8qr6TgU7wyu3p8Ze1TMmHsUz8FMxCiHWY8z56UQx+bMl2G7E3OMvE/AEJrYiGGv+Hnbgpci+RYqeTcUUygbeX0xlfPgy5JTafvY2UlvVNYzTsFrKqhpj0xBrQNH4q8DRx6NSeoVy9NH3m5uhqHp+/t4CBzPAdfmwdFHxkN7N7+1hr7rrdH+L2+N9gkN7YqfOxS/lRS/8hLx++gXTODcOi2NISc3udqcls/L0xKcmZbWqNISXP9qMx9m9zNvhJu2cmyPbpp8+MuOPb3aIcur268+ja3z8cy6/ws= \ No newline at end of file diff --git a/examples/figures/simple-id.svg b/examples/figures/simple-id.svg index 0b5eba7d..2fc5a9ce 100644 --- a/examples/figures/simple-id.svg +++ b/examples/figures/simple-id.svg @@ -1,3 +1,3 @@ -
2 \\ \{1, 2\}
1 \\ \{1, 2\}
5
3 \\ \{1, 2\}
4 \\ \{1, 2\}
Viewer does not support full SVG 1.1
\ No newline at end of file +
B \\ \{x, y\}
A \\ \{a, b\}
V
C \\ \{v, w\}
D \\ \{k,l \...
Viewer does not support full SVG 1.1
\ No newline at end of file From b2c27c0fb38d19df19c1c29205d042ee3f0283b9 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 7 Sep 2021 13:38:33 +0300 Subject: [PATCH 096/133] Added check that all utility values are filled in utility matrix. --- src/influence_diagram.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 0189c4df..789f094c 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -741,7 +741,10 @@ function add_utilities!(diagram::InfluenceDiagram, node::Name, utilities::Abstra if v ∈ [j.v for j in diagram.Y] throw(DomainError("Utilities should be added only once for each node.")) end - + if any(u ==Inf for u in utilities) + throw(DomainError("Utility values should be less than infinity.")) + end + if size(utilities) == Tuple((diagram.S[j] for j in diagram.I_j[v])) if isa(utilities, UtilityMatrix) push!(diagram.Y, Utilities(Node(v), utilities.matrix)) From 1b560085025719c34b08b34549a4afc6e236e3c8 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 7 Sep 2021 13:39:18 +0300 Subject: [PATCH 097/133] Updated testing of influence diagram. --- test/influence_diagram.jl | 198 +++++++++++++++++++++++++------------- 1 file changed, 133 insertions(+), 65 deletions(-) diff --git a/test/influence_diagram.jl b/test/influence_diagram.jl index 26bf673d..af0c5b70 100644 --- a/test/influence_diagram.jl +++ b/test/influence_diagram.jl @@ -2,90 +2,158 @@ using Test, Logging, Random using DecisionProgramming @info "Testing ChandeNode" -@test isa(ChanceNode(1, Node[]), ChanceNode) -@test isa(ChanceNode(2, Node[1]), ChanceNode) -@test_throws DomainError ChanceNode(1, Node[1]) -@test_throws DomainError ChanceNode(1, Node[2]) +@test isa(ChanceNode("A", [], ["a", "b"]), ChanceNode) +@test isa(ChanceNode("B", [], ["x", "y", "z"]), ChanceNode) +@test_throws MethodError ChanceNode(1, [], ["x", "y", "z"]) +@test_throws MethodError ChanceNode("B", [1], ["y", "z"]) +@test_throws MethodError ChanceNode("B", ["A"], [1, "y", "z"]) +@test_throws MethodError ChanceNode("B", ["A"]) @info "Testing DecisionNode" -@test isa(DecisionNode(1, Node[]), DecisionNode) -@test isa(DecisionNode(2, Node[1]), DecisionNode) -@test_throws DomainError DecisionNode(1, Node[1]) -@test_throws DomainError DecisionNode(1, Node[2]) +@test isa(DecisionNode("D", [], ["x", "y"]), DecisionNode) +@test isa(DecisionNode("E", ["C"], ["x", "y"]), DecisionNode) +@test_throws MethodError DecisionNode(1, [], ["x", "y", "z"]) +@test_throws MethodError DecisionNode("D", [1], ["y", "z"]) +@test_throws MethodError DecisionNode("D", ["A"], [1, "y", "z"]) +@test_throws MethodError DecisionNode("D", ["A"]) @info "Testing ValueNode" -@test isa(ValueNode(1, Node[]), ValueNode) -@test isa(ValueNode(2, Node[1]), ValueNode) -@test_throws DomainError ValueNode(1, Node[1]) -@test_throws DomainError ValueNode(1, Node[2]) +@test isa(ValueNode("V", []), ValueNode) +@test isa(ValueNode("V", ["E", "D"]), ValueNode) +@test_throws MethodError ValueNode(1, []) +@test_throws MethodError ValueNode("V", [2]) @info "Testing State" -@test isa(States([1, 2, 3]), States) -@test_throws DomainError States([0, 1]) -@test States([(2, [1, 3]), (3, [2, 4, 5])]) == States([2, 3, 2, 3, 3]) - -@info "Testing validate_influence_diagram" -@test_throws DomainError validate_influence_diagram( - States([1]), - [ChanceNode(1, Node[])], - [DecisionNode(2, Node[])], - [ValueNode(3, [1, 2])] -) -@test_throws DomainError validate_influence_diagram( - States([1, 1]), - [ChanceNode(1, Node[])], - [DecisionNode(1, Node[])], - [ValueNode(3, [1, 2])] -) -@test_throws DomainError validate_influence_diagram( - States([1, 1]), - [ChanceNode(1, Node[])], - [DecisionNode(2, Node[])], - [ValueNode(2, [1])] -) -@test_throws DomainError validate_influence_diagram( - States([1, 1]), - [ChanceNode(1, Node[])], - [DecisionNode(2, Node[])], - [ValueNode(3, [2]), ValueNode(4, [3])] -) -# Test redundancy -@test validate_influence_diagram( - States([1, 1]), - [ChanceNode(1, Node[])], - [DecisionNode(2, Node[])], - [ValueNode(3, Node[])] -) === nothing +@test isa(States(State[1, 2, 3]), States) +@test_throws DomainError States(State[0, 1]) @info "Testing paths" -@test vec(collect(paths(States([2, 3])))) == [(1, 1), (2, 1), (1, 2), (2, 2), (1, 3), (2, 3)] -@test vec(collect(paths(States([2, 3]), Dict(1=>2)))) == [(2, 1), (2, 2), (2, 3)] +@test vec(collect(paths(States(State[2, 3])))) == [(1, 1), (2, 1), (1, 2), (2, 2), (1, 3), (2, 3)] +@test vec(collect(paths(States(State[2, 3]), Dict(Node(1)=>State(2))))) == [(2, 1), (2, 2), (2, 3)] +@test vec(collect(paths(States(State[2, 3]), FixedPath(Dict(Node(1)=>State(2)))))) == [(2, 1), (2, 2), (2, 3)] @info "Testing Probabilities" -@test isa(Probabilities(1, [0.4 0.6; 0.3 0.7]), Probabilities) -@test isa(Probabilities(1, [0.0, 0.4, 0.6]), Probabilities) -@test_throws DomainError Probabilities(1, [1.1, 0.1]) +@test isa(Probabilities(Node(1), [0.4 0.6; 0.3 0.7]), Probabilities) +@test isa(Probabilities(Node(1), [0.0, 0.4, 0.6]), Probabilities) +@test_throws DomainError Probabilities(Node(1), [1.1, 0.1]) @info "Testing DefaultPathProbability" P = DefaultPathProbability( - [ChanceNode(1, Node[]), ChanceNode(2, [1])], - [Probabilities(1, [0.4, 0.6]), Probabilities(2, [0.3 0.7; 0.9 0.1])] + [Node(1), Node(2)], + [Node[], [Node(1)]], + [Probabilities(Node(1), [0.4, 0.6]), Probabilities(Node(2), [0.3 0.7; 0.9 0.1])] ) @test isa(P, DefaultPathProbability) -@test P((1, 2)) == 0.4 * 0.7 +@test P((State(1), State(2))) == 0.4 * 0.7 -@info "Testing Consequences" -@test isa(Consequences(1, [-1.1, 0.0, 2.7]), Consequences) -@test isa(Consequences(1, [-1.1 0.0; 2.7 7.0]), Consequences) +@info "Testing Utilities" +@test isa(Utilities(Node(1), Utility[-1.1, 0.0, 2.7]), Utilities) +@test isa(Utilities(Node(1), Utility[-1.1 0.0; 2.7 7.0]), Utilities) @info "Testing DefaultPathUtility" U = DefaultPathUtility( - [ValueNode(3, [2]), ValueNode(4, [1, 2])], - [Consequences(3, [1.0, 1.4]), Consequences(4, [1.0 1.5; 0.6 3.4])] + [Node[2], Node[1, 2]], + [Utilities(Node(3), Utility[1.0, 1.4]), Utilities(Node(4), Utility[1.0 1.5; 0.6 3.4])] ) @test isa(U, DefaultPathUtility) -@test U((2, 1)) == 1.0 + 0.6 +@test U((State(2), State(1))) == Utility(1.0 + 0.6) + +@info "Testing InfluenceDiagram" +diagram = InfluenceDiagram() +@test isa(diagram, InfluenceDiagram) +push!(diagram.Nodes, ChanceNode("A", [], ["a", "b"])) +@test isa(diagram, InfluenceDiagram) + +@info "Testing add_node! and validate_node" +diagram = InfluenceDiagram() +add_node!(diagram, ChanceNode("A", [], ["a", "b"])) +@test isa(diagram.Nodes[1], ChanceNode) +@test_throws DomainError add_node!(diagram, ChanceNode("A", [], ["a", "b"])) +@test_throws DomainError add_node!(diagram, ChanceNode("C", ["B", "B"], ["a", "b"])) +@test_throws DomainError add_node!(diagram, ChanceNode("C", ["C", "B"], ["a", "b"])) +@test_throws DomainError add_node!(diagram, ChanceNode("C", [], ["a"])) +@test_throws DomainError add_node!(diagram, DecisionNode("A", [], ["a", "b"])) +@test_throws DomainError add_node!(diagram, DecisionNode("C", ["B", "B"], ["a", "b"])) +@test_throws DomainError add_node!(diagram, DecisionNode("C", ["C", "B"], ["a", "b"])) +@test_throws DomainError add_node!(diagram, DecisionNode("C", [], ["a"])) +@test_throws DomainError add_node!(diagram, ValueNode("A", [])) +@test_throws DomainError add_node!(diagram, ValueNode("C", ["B", "B"])) +@test_throws DomainError add_node!(diagram, ValueNode("C", ["C", "B"])) +add_node!(diagram, ChanceNode("C", ["A"], ["a", "b"])) +add_node!(diagram, DecisionNode("D", ["A"], ["c", "d"])) +add_node!(diagram, ValueNode("V", ["A"])) +@test length(diagram.Nodes) == 4 +@test isa(diagram.Nodes[3], DecisionNode) +@test isa(diagram.Nodes[4], ValueNode) + +@info "Testing generate_arcs!" +diagram = InfluenceDiagram() +@test_throws DomainError generate_arcs!(diagram) +add_node!(diagram, ChanceNode("A", [], ["a", "b"])) +add_node!(diagram, ChanceNode("C", ["A"], ["a", "b", "c"])) +add_node!(diagram, ValueNode("V", ["A", "C"])) +generate_arcs!(diagram) +@test diagram.Names == ["A", "C", "V"] +@test diagram.I_j == [[], Node[1], Node[1, 2]] +@test diagram.States == [["a", "b"], ["a", "b", "c"]] +@test diagram.S == [State(2), State(3)] +@test diagram.C == Node[1, 2] +@test diagram.D == Node[] +@test diagram.V == Node[3] +@test diagram.X == Probabilities[] +@test diagram.Y == Utilities[] + + +@info "Testing ProbabilityMatrix" +diagram = InfluenceDiagram() +add_node!(diagram, ChanceNode("A", [], ["a", "b"])) +add_node!(diagram, ChanceNode("B", ["A"], ["a", "b", "c"])) +add_node!(diagram, DecisionNode("D", ["A"], ["a", "b", "c"])) +generate_arcs!(diagram) +@test ProbabilityMatrix(diagram, "A") == zeros(2) +@test ProbabilityMatrix(diagram, "B") == zeros(2, 3) +@test_throws DomainError ProbabilityMatrix(diagram, "C") +@test_throws DomainError ProbabilityMatrix(diagram, "D") +X_A = ProbabilityMatrix(diagram, "A") +set_probability!(X_A, ["a"], 0.2) +@test X_A == [0.2, 0] +set_probability!(X_A, ["b"], 0.9) +@test X_A == [0.2, 0.9] +@test_throws DomainError add_probabilities!(diagram, "A", X_A) +set_probability!(X_A, ["b"], 0.8) +@test add_probabilities!(diagram, "A", X_A) == [[0.2, 0.8]] +@test_throws DomainError add_probabilities!(diagram, "A", X_A) + +@info "Testing UtilityMatrix" +diagram = InfluenceDiagram() +add_node!(diagram, ChanceNode("A", [], ["a", "b"])) +add_node!(diagram, DecisionNode("D", ["A"], ["a", "b", "c"])) +add_node!(diagram, ValueNode("V", ["A", "D"])) +generate_arcs!(diagram) +@test UtilityMatrix(diagram, "V") == fill(Inf, (2, 3)) +@test_throws DomainError UtilityMatrix(diagram, "C") +@test_throws DomainError UtilityMatrix(diagram, "D") +Y_V = UtilityMatrix(diagram, "V") +@test_throws DomainError add_utilities!(diagram, "V", Y_V) +set_utility!(Y_V, ["a", :], [1, 2, 3]) +set_utility!(Y_V, ["b", "c"], 4) +set_utility!(Y_V, ["b", "a"], 5) +set_utility!(Y_V, ["b", "b"], 6) +@test Y_V == [1 2 3; 5 6 4] +add_utilities!(diagram, "V", Y_V) +@test diagram.Y == [[1 2 3; 5 6 4]] +@test_throws DomainError add_utilities!(diagram, "V", Y_V) + -@info "Testing LocalDecisionStrategy" -@test_throws DomainError LocalDecisionStrategy(1, [0, 0, 2]) -@test_throws DomainError LocalDecisionStrategy(1, [0, 1, 1]) +@info "Testing generate_diagram!" +diagram = InfluenceDiagram() +add_node!(diagram, ChanceNode("A", [], ["a", "b"])) +add_node!(diagram, DecisionNode("D", ["A"], ["a", "b", "c"])) +add_node!(diagram, ValueNode("V", ["A", "D"])) +generate_arcs!(diagram) +add_utilities!(diagram, "V", [-1 2 3; 5 6 4]) +add_probabilities!(diagram, "A", [0.2, 0.8]) +generate_diagram!(diagram) +@test diagram.translation == Utility(0) +generate_diagram!(diagram, positive_path_utility=true) +@test diagram.translation == Utility(2) From ad01e56498a6bab223d2616566486318ed7827a7 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 7 Sep 2021 14:08:36 +0300 Subject: [PATCH 098/133] Added a few broken influence diagrams to generate_arcs! testing. --- test/influence_diagram.jl | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/influence_diagram.jl b/test/influence_diagram.jl index af0c5b70..453369d7 100644 --- a/test/influence_diagram.jl +++ b/test/influence_diagram.jl @@ -103,6 +103,28 @@ generate_arcs!(diagram) @test diagram.X == Probabilities[] @test diagram.Y == Utilities[] +#Non-existent node B +diagram = InfluenceDiagram() +add_node!(diagram, ChanceNode("A", [], ["a", "b"])) +add_node!(diagram, ChanceNode("C", ["B"], ["a", "b", "c"])) +add_node!(diagram, ValueNode("V", ["A", "C"])) +@test_throws DomainError generate_arcs!(diagram) + +#Cylic +diagram = InfluenceDiagram() +add_node!(diagram, ChanceNode("A", ["R"], ["a", "b"])) +add_node!(diagram, ChanceNode("R", ["C", "A"], ["a", "b", "c"])) +add_node!(diagram, ChanceNode("C", ["A"], ["a", "b", "c"])) +add_node!(diagram, ValueNode("V", ["A", "C"])) +@test_throws DomainError generate_arcs!(diagram) + +#Value node in I_j +diagram = InfluenceDiagram() +add_node!(diagram, ChanceNode("A", ["R"], ["a", "b"])) +add_node!(diagram, ChanceNode("R", ["C", "A"], ["a", "b", "c"])) +add_node!(diagram, ChanceNode("C", ["A", "V"], ["a", "b", "c"])) +add_node!(diagram, ValueNode("V", ["A"])) +@test_throws DomainError generate_arcs!(diagram) @info "Testing ProbabilityMatrix" diagram = InfluenceDiagram() From 4a701533cae541c2e3f628534b143974c9004938 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Tue, 7 Sep 2021 14:10:46 +0300 Subject: [PATCH 099/133] Corrected from subsetneq to subseteq. Instances were this has effect is if the influence diagram does not have value nodes, or the value node has been erroneously put into the I_j of some other node, then we don't want it to produce this error message but the correct one under this one. --- src/influence_diagram.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 789f094c..56a95357 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -744,7 +744,7 @@ function add_utilities!(diagram::InfluenceDiagram, node::Name, utilities::Abstra if any(u ==Inf for u in utilities) throw(DomainError("Utility values should be less than infinity.")) end - + if size(utilities) == Tuple((diagram.S[j] for j in diagram.I_j[v])) if isa(utilities, UtilityMatrix) push!(diagram.Y, Utilities(Node(v), utilities.matrix)) @@ -765,7 +765,7 @@ function validate_structure(Nodes::Vector{AbstractNode}, C_and_D::Vector{Abstrac if n_CD == 0 throw(DomainError("The influence diagram must have chance or decision nodes.")) end - if !(union((n.I_j for n in Nodes)...) ⊊ Set(n.name for n in Nodes)) + if !(union((n.I_j for n in Nodes)...) ⊆ Set(n.name for n in Nodes)) throw(DomainError("Each node that is part of an information set should be added as a node.")) end # Checking the information sets of C and D nodes From 7dd7141cb000a6ea3d271f10334fc66b1b1bcce4 Mon Sep 17 00:00:00 2001 From: Olli Herrala <43684983+solliolli@users.noreply.github.com> Date: Fri, 10 Sep 2021 10:21:11 +0300 Subject: [PATCH 100/133] Added length function Added a length function to path compatibility variables (AbstractDict) so it prints nicely at least in Atom --- src/decision_model.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/decision_model.jl b/src/decision_model.jl index 3ce8a89a..989fdf1e 100644 --- a/src/decision_model.jl +++ b/src/decision_model.jl @@ -60,6 +60,7 @@ struct PathCompatibilityVariables{N} <: AbstractDict{Path{N}, VariableRef} data::Dict{Path{N}, VariableRef} end +Base.length(x_s::PathCompatibilityVariables) = length(x_s.data) Base.getindex(x_s::PathCompatibilityVariables, key) = getindex(x_s.data, key) Base.get(x_s::PathCompatibilityVariables, key, default) = get(x_s.data, key, default) Base.keys(x_s::PathCompatibilityVariables) = keys(x_s.data) From c01346c6c79d0bd2cdc16a299da8f3cff0f9f737 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Mon, 13 Sep 2021 19:45:29 +0300 Subject: [PATCH 101/133] Updated random.jl and added the necessary functions to DecisionProgramming.jl --- src/DecisionProgramming.jl | 5 +- src/random.jl | 130 ++++++++++++++++++++++--------------- 2 files changed, 81 insertions(+), 54 deletions(-) diff --git a/src/DecisionProgramming.jl b/src/DecisionProgramming.jl index 31efeb99..4dcc6ba5 100644 --- a/src/DecisionProgramming.jl +++ b/src/DecisionProgramming.jl @@ -45,7 +45,10 @@ export DecisionVariables, expected_value, conditional_value_at_risk -export random_diagram +export random_diagram!, + random_probabilities!, + random_utilities!, + LocalDecisionStrategy export CompatiblePaths, UtilityDistribution, diff --git a/src/random.jl b/src/random.jl index 77bc0695..0e3f6289 100644 --- a/src/random.jl +++ b/src/random.jl @@ -5,9 +5,10 @@ using Random Generates random information sets for chance and decision nodes. """ -function information_set(rng::AbstractRNG, j::Int, n_I::Int) +function information_set(rng::AbstractRNG, j::Node, n_I::Int) m = min(rand(rng, 0:n_I), j-1) - return shuffle(rng, 1:(j-1))[1:m] + I_j = shuffle(rng, 1:(j-1))[1:m] + return sort(I_j) end """ @@ -24,22 +25,35 @@ function information_set(rng::AbstractRNG, leaf_nodes::Vector{Node}, n::Int) else m = rand(rng, 0:l) end - return [leaf_nodes; non_leaf_nodes[1:m]] + I_v = [leaf_nodes; non_leaf_nodes[1:m]] + return sort(I_v) end + """ - function random_diagram(rng::AbstractRNG, n_C::Int, n_D::Int, n_V::Int, m_C::Int, m_D::Int) + function random_diagram!(rng::AbstractRNG, n_C::Int, n_D::Int, n_V::Int, m_C::Int, m_D::Int, states::Vector{Int}) Generate random decision diagram with `n_C` chance nodes, `n_D` decision nodes, and `n_V` value nodes. Parameter `m_C` and `m_D` are the upper bounds for the size of the information set. +# Arguments +- `rng::AbstractRNG`: Random number generator. +- `n_C::Int`: Number of chance nodes. +- `n_D::Int`: Number of decision nodes. +- `m_C::Int`: Upper bound for size of information set for chance nodes. +- `m_D::Int`: Upper bound for size of information set for decision nodes. +- `states::Vector{State}`: The number of states for each chance and decision node + is randomly chosen from this set of numbers. + + # Examples ```julia rng = MersenneTwister(3) -random_diagram(rng, 5, 2, 3, 2) +diagram = InfluenceDiagram() +random_diagram!(rng, diagram, 5, 2, 3, 2, [2, 4, 5]) ``` """ -function random_diagram(rng::AbstractRNG, n_C::Int, n_D::Int, n_V::Int, m_C::Int, m_D::Int) +function random_diagram!(rng::AbstractRNG, diagram::InfluenceDiagram, n_C::Int, n_D::Int, n_V::Int, m_C::Int, m_D::Int, states::Vector{Int}) n = n_C + n_D n_C ≥ 0 || throw(DomainError("There should be `n_C ≥ 0` chance nodes.")) n_D ≥ 0 || throw(DomainError("There should be `n_D ≥ 0` decision nodes")) @@ -47,44 +61,43 @@ function random_diagram(rng::AbstractRNG, n_C::Int, n_D::Int, n_V::Int, m_C::Int n_V ≥ 1 || throw(DomainError("There should be `n_V ≥ 1` value nodes.")) m_C ≥ 1 || throw(DomainError("Maximum size of information set should be `m_C ≥ 1`.")) m_D ≥ 1 || throw(DomainError("Maximum size of information set should be `m_D ≥ 1`.")) + all(s > 1 for s in states) || throw(DomainError("Minimum number of states possible should be 2.")) # Create node indices U = shuffle(rng, 1:n) - C_j = sort(U[1:n_C]) - D_j = sort(U[(n_C+1):n]) - V_j = collect((n+1):(n+n_V)) + diagram.C = [Node(c) for c in sort(U[1:n_C])] + diagram.D = [Node(d) for d in sort(U[(n_C+1):n])] + diagram.V = [Node(v) for v in collect((n+1):(n+n_V))] + diagram.I_j = Vector{Vector{Node}}(undef, n+n_V) # Create chance and decision nodes - C = [ChanceNode(j, information_set(rng, j, m_C)) for j in C_j] - D = [DecisionNode(j, information_set(rng, j, m_D)) for j in D_j] + for c in diagram.C + diagram.I_j[c] = information_set(rng, c, m_C) + end + for d in diagram.D + diagram.I_j[d] = information_set(rng, d, m_D) + end + # Assign each leaf node to a random value node - leaf_nodes = setdiff(1:n, (c.I_j for c in C)..., (d.I_j for d in D)...) - leaf_nodes_j = Dict(j=>Node[] for j in V_j) - for i in leaf_nodes - k = rand(rng, V_j) - push!(leaf_nodes_j[k], i) + leaf_nodes = setdiff(1:n, (diagram.I_j[c] for c in diagram.C)..., (diagram.I_j[d] for d in diagram.D)...) + leaf_nodes_v = Dict(v=>Node[] for v in diagram.V) + for j in leaf_nodes + v = rand(rng, diagram.V) + push!(leaf_nodes_v[v], j) end # Create values nodes - V = [ValueNode(j, information_set(rng, leaf_nodes_j[j], n)) for j in V_j] + for v in diagram.V + diagram.I_j[v] = information_set(rng, leaf_nodes_v[v], n) + end - return C, D, V -end -""" - function States(rng::AbstractRNG, states::Vector{State}, n::Int) + diagram.S = States(State[rand(rng, states, n)...]) + diagram.X = Vector{Probabilities}(undef, n_C) + diagram.Y = Vector{Utilities}(undef, n_V) -Generate `n` random states from `states`. - -# Examples -```julia -rng = MersenneTwister(3) -S = States(rng, [2, 3], 10) -``` -""" -function States(rng::AbstractRNG, states::Vector{State}, n::Int) - States(rand(rng, states, n)) + return diagram end """ @@ -95,14 +108,16 @@ Generate random probabilities for chance node `c` with `S` states. # Examples ```julia rng = MersenneTwister(3) -c = ChanceNode(2, [1]) -S = States([2, 2]) -Probabilities(rng, c, S) +diagram = InfluenceDiagram() +random_diagram!(rng, diagram, 5, 2, 3, 2, [2, 4, 5]) +c = diagram.C[1] +Probabilities!(rng, diagram, c) ``` """ -function Probabilities(rng::AbstractRNG, c::ChanceNode, S::States; n_inactive::Int=0) - states = S[c.I_j] - state = S[c.j] +function random_probabilities!(rng::AbstractRNG, diagram::InfluenceDiagram, c::Node; n_inactive::Int=0) + I_c = diagram.I_j[c] + states = diagram.S[I_c] + state = diagram.S[c] if !(0 ≤ n_inactive ≤ prod([states...; (state - 1)])) throw(DomainError("Number of inactive states must be < prod([S[I_j]...;, S[j]-1])")) end @@ -133,37 +148,45 @@ function Probabilities(rng::AbstractRNG, c::ChanceNode, S::States; n_inactive::I data[s, :] /= sum(data[s, :]) end - Probabilities(c.j, data) + index_c = findfirst(j -> j==c, diagram.C) + diagram.X[index_c] = Probabilities(c, data) end -scale(x::Float64, low::Float64, high::Float64) = x * (high - low) + low +scale(x::Utility, low::Utility, high::Utility) = x * (high - low) + low """ - function Consequences(rng::AbstractRNG, v::ValueNode, S::States; low::Float64=-1.0, high::Float64=1.0) + function Utilities!(rng::AbstractRNG, diagram::InfluenceDiagram, v::Node; low::Float64=-1.0, high::Float64=1.0) -Generate random consequences between `low` and `high` for value node `v` with `S` states. +Generate random utilities between `low` and `high` for value node `v`. # Examples ```julia rng = MersenneTwister(3) -v = ValueNode(3, [1]) -S = States([2, 2]) -Consequences(rng, v, S; low=-1.0, high=1.0) +diagram = InfluenceDiagram() +random_diagram!(rng, diagram, 5, 2, 3, 2, [2, 4, 5]) +v = diagram.V[1] +Utilities!(rng, diagram, v) ``` """ -function Consequences(rng::AbstractRNG, v::ValueNode, S::States; low::Float64=-1.0, high::Float64=1.0) +function random_utilities!(rng::AbstractRNG, diagram::InfluenceDiagram, v::Node; low::Float64=-1.0, high::Float64=1.0) if !(high > low) throw(DomainError("high should be greater than low")) end - data = rand(rng, S[v.I_j]...) - data = scale.(data, low, high) - Consequences(v.j, data) + I_v = diagram.I_j[v] + data = rand(rng, Utility, diagram.S[I_v]...) + data = scale.(data, Utility(low), Utility(high)) + + index_v = findfirst(j -> j==v, diagram.V) + diagram.Y[index_v] = Utilities(v, data) end + + + """ function LocalDecisionStrategy(rng::AbstractRNG, d::DecisionNode, S::States) -Generate random decision strategy for decision node `d` with `S` states. +Generate random decision strategy for decision node `d`. # Examples ```julia @@ -173,13 +196,14 @@ S = States([2, 2]) LocalDecisionStrategy(rng, d, S) ``` """ -function LocalDecisionStrategy(rng::AbstractRNG, d::DecisionNode, S::States) - states = S[d.I_j] - state = S[d.j] +function LocalDecisionStrategy(rng::AbstractRNG, diagram::InfluenceDiagram, d::Node) + I_d = diagram.I_j[d] + states = diagram.S[I_d] + state = diagram.S[d] data = zeros(Int, states..., state) for s in CartesianIndices((states...,)) s_j = rand(rng, 1:state) data[s, s_j] = 1 end - LocalDecisionStrategy(d.j, data) + LocalDecisionStrategy(d, data) end From b8774b5da9636fc3be5d518f3d48db01270f8169 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 16 Sep 2021 11:19:28 +0300 Subject: [PATCH 102/133] Updated cpp example. --- examples/contingent-portfolio-programming.jl | 102 ++++++++----------- 1 file changed, 42 insertions(+), 60 deletions(-) diff --git a/examples/contingent-portfolio-programming.jl b/examples/contingent-portfolio-programming.jl index 8d4baa2b..26f39b19 100644 --- a/examples/contingent-portfolio-programming.jl +++ b/examples/contingent-portfolio-programming.jl @@ -4,43 +4,31 @@ using DecisionProgramming Random.seed!(42) -const dᴾ = 1 # Decision node: range for number of patents -const cᵀ = 2 # Chance node: technical competitiveness -const dᴬ = 3 # Decision node: range for number of applications -const cᴹ = 4 # Chance node: market share -const DP_states = ["0-3 patents", "3-6 patents", "6-9 patents"] -const CT_states = ["low", "medium", "high"] -const DA_states = ["0-5 applications", "5-10 applications", "10-15 applications"] -const CM_states = ["low", "medium", "high"] - -S = States([ - (length(DP_states), [dᴾ]), - (length(CT_states), [cᵀ]), - (length(DA_states), [dᴬ]), - (length(CM_states), [cᴹ]), -]) -C = Vector{ChanceNode}() -D = Vector{DecisionNode}() -V = Vector{ValueNode}() -X = Vector{Probabilities}() -Y = Vector{Consequences}() - -I_DP = Vector{Node}() -push!(D, DecisionNode(dᴾ, I_DP)) - -I_CT = [dᴾ] -X_CT = zeros(S[dᴾ], S[cᵀ]) +function num_states(diagram::InfluenceDiagram, node::Name) + idx = findfirst(isequal(node), diagram.Names) + if isnothing(idx) + throw(DomainError("Name $node not found in the diagram.")) + end + return diagram.S[idx] +end + +@info("Creating the influence diagram.") +diagram = InfluenceDiagram() + +add_node!(diagram, DecisionNode("DP", [], ["0-3 patents", "3-6 patents", "6-9 patents"])) +add_node!(diagram, ChanceNode("CT", ["DP"], ["low", "medium", "high"])) +add_node!(diagram, DecisionNode("DA", ["DP", "CT"], ["0-5 applications", "5-10 applications", "10-15 applications"])) +add_node!(diagram, ChanceNode("CM", ["CT", "DA"], ["low", "medium", "high"])) + +generate_arcs!(diagram) + +X_CT = ProbabilityMatrix(diagram, "CT") X_CT[1, :] = [1/2, 1/3, 1/6] X_CT[2, :] = [1/3, 1/3, 1/3] X_CT[3, :] = [1/6, 1/3, 1/2] -push!(C, ChanceNode(cᵀ, I_CT)) -push!(X, Probabilities(cᵀ, X_CT)) +add_probabilities!(diagram, "CT", X_CT) -I_DA = [dᴾ, cᵀ] -push!(D, DecisionNode(dᴬ, I_DA)) - -I_CM = [cᵀ, dᴬ] -X_CM = zeros(S[cᵀ], S[dᴬ], S[cᴹ]) +X_CM = ProbabilityMatrix(diagram, "CM") X_CM[1, 1, :] = [2/3, 1/4, 1/12] X_CM[1, 2, :] = [1/2, 1/3, 1/6] X_CM[1, 3, :] = [1/3, 1/3, 1/3] @@ -50,23 +38,15 @@ X_CM[2, 3, :] = [1/6, 1/3, 1/2] X_CM[3, 1, :] = [1/3, 1/3, 1/3] X_CM[3, 2, :] = [1/6, 1/3, 1/2] X_CM[3, 3, :] = [1/12, 1/4, 2/3] -push!(C, ChanceNode(cᴹ, I_CM)) -push!(X, Probabilities(cᴹ, X_CM)) +add_probabilities!(diagram, "CM", X_CM) -# Dummy value node -push!(V, ValueNode(5, [cᴹ])) -push!(Y, Consequences(5, zeros(S[cᴹ]))) +generate_diagram!(diagram, default_utility=false) -@info("Validate influence diagram.") -validate_influence_diagram(S, C, D, V) -sort!.((C, D, V, X, Y), by = x -> x.j) -@info("Creating path probability.") -P = DefaultPathProbability(C, X) -@info("Defining DecisionModel") +@info("Creating the decision model.") model = Model() -z = DecisionVariables(model, S, D) +z = DecisionVariables(model, diagram) @info("Creating problem specific constraints and expressions") @@ -85,12 +65,12 @@ O_t = rand(1:3,n_T) # number of patents for each tech project I_a = rand(n_T)*2 # costs of application projects O_a = rand(2:4,n_T) # number of applications for each appl. project -V_A = rand(S[cᴹ], n_A).+0.5 # Value of an application +V_A = rand(num_states(diagram, "CM"), n_A).+0.5 # Value of an application V_A[1, :] .+= -0.5 # Low market share: less value V_A[3, :] .+= 0.5 # High market share: more value -x_T = variables(model, [S[dᴾ]...,n_T]; binary=true) -x_A = variables(model, [S[dᴾ]...,S[cᵀ]...,S[dᴬ]..., n_A]; binary=true) +x_T = variables(model, [num_states(diagram, "DP"), n_T]; binary=true) +x_A = variables(model, [num_states(diagram, "DP"), num_states(diagram, "CT"), num_states(diagram, "DA"), n_A]; binary=true) M = 20 # a large constant ε = 0.5*minimum([O_t O_a]) # a helper variable, allows using ≤ instead of < in constraints (28b) and (29b) @@ -125,10 +105,11 @@ z_dA = z.z[2] @constraint(model, [i=1:3, j=1:3, k=1:3], x_A[i,j,k,2] <= x_T[i,2]) @info("Creating model objective.") -patent_investment_cost = @expression(model, [i=1:S[1]], sum(x_T[i, t] * I_t[t] for t in 1:n_T)) -application_investment_cost = @expression(model, [i=1:S[1], j=1:S[2], k=1:S[3]], sum(x_A[i, j, k, a] * I_a[a] for a in 1:n_A)) -application_value = @expression(model, [i=1:S[1], j=1:S[2], k=1:S[3], l=1:S[4]], sum(x_A[i, j, k, a] * V_A[l, a] for a in 1:n_A)) -@objective(model, Max, sum( sum( P((i,j,k,l)) * (application_value[i,j,k,l] - application_investment_cost[i,j,k]) for j in 1:S[2], k in 1:S[3], l in 1:S[4] ) - patent_investment_cost[i] for i in 1:S[1] )) +patent_investment_cost = @expression(model, [i=1:diagram.S[1]], sum(x_T[i, t] * I_t[t] for t in 1:n_T)) +application_investment_cost = @expression(model, [i=1:diagram.S[1], j=1:diagram.S[2], k=1:diagram.S[3]], sum(x_A[i, j, k, a] * I_a[a] for a in 1:n_A)) +application_value = @expression(model, [i=1:diagram.S[1], j=1:diagram.S[2], k=1:diagram.S[3], l=1:diagram.S[4]], sum(x_A[i, j, k, a] * V_A[l, a] for a in 1:n_A)) +@objective(model, Max, sum( sum( diagram.P((i,j,k,l)) * (application_value[i,j,k,l] - application_investment_cost[i,j,k]) for j in State(1):diagram.S[2], k in State(1):diagram.S[3], l in State(1):diagram.S[4] ) - patent_investment_cost[i] for i in State(1):diagram.S[1] )) + @info("Starting the optimization process.") optimizer = optimizer_with_attributes( @@ -141,29 +122,30 @@ optimize!(model) @info("Extracting results.") Z = DecisionStrategy(z) +S_probabilities = StateProbabilities(diagram, Z) @info("Printing decision strategy:") -print_decision_strategy(S, Z) +print_decision_strategy(diagram, Z, S_probabilities) @info("Extracting path utilities") struct PathUtility <: AbstractPathUtility data::Array{AffExpr} end -Base.getindex(U::PathUtility, i::Int) = getindex(U.data, i) -Base.getindex(U::PathUtility, I::Vararg{Int,N}) where N = getindex(U.data, I...) +Base.getindex(U::PathUtility, i::State) = getindex(U.data, i) +Base.getindex(U::PathUtility, I::Vararg{State,N}) where N = getindex(U.data, I...) (U::PathUtility)(s::Path) = value.(U[s...]) path_utility = [@expression(model, sum(x_A[s[1:3]..., a] * (V_A[s[4], a] - I_a[a]) for a in 1:n_A) - - sum(x_T[s[1], t] * I_t[t] for t in 1:n_T)) for s in paths(S)] -U = PathUtility(path_utility) + sum(x_T[s[1], t] * I_t[t] for t in 1:n_T)) for s in paths(diagram.S)] +diagram.U = PathUtility(path_utility) @info("Computing utility distribution.") -udist = UtilityDistribution(S, P, U, Z) +U_distribution = UtilityDistribution(diagram, Z) @info("Printing utility distribution.") -print_utility_distribution(udist) +print_utility_distribution(U_distribution) @info("Printing statistics") -print_statistics(udist) +print_statistics(U_distribution) From d318cfb48bd1ff141e234d4d2e90cc66184d80b4 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 16 Sep 2021 11:28:21 +0300 Subject: [PATCH 103/133] Created wrapper function for StateProbabilities so that it can be also called by node and state indices if wanted. For instance in testing. --- src/analysis.jl | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/analysis.jl b/src/analysis.jl index cd39afa9..b0540d58 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -153,10 +153,36 @@ struct StateProbabilities fixed::FixedPath end +""" + StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy, node::Node, state::State, prior_probabilities::StateProbabilities) + +Associate each node with array of conditional probabilities for each of its states occuring in compatible paths given + fixed states and prior probability. Fix node and state using their indices. + +# Examples +```julia +# Prior probabilities +julia> prior_probabilities = StateProbabilities(diagram, Z) +julia> StateProbabilities(diagram, Z, Node(2), State(1), prior_probabilities) +``` +""" +function StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy, node::Node, state::State, prior_probabilities::StateProbabilities) + prior = prior_probabilities.probs[node][state] + fixed = deepcopy(prior_probabilities.fixed) + + push!(fixed, node => state) + probs = Dict(i => zeros(diagram.S[i]) for i in 1:length(diagram.S)) + for s in CompatiblePaths(diagram, Z, fixed), i in 1:length(diagram.S) + probs[i][s[i]] += diagram.P(s) / prior + end + StateProbabilities(probs, fixed) +end + """ StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy, node::Name, state::Name, prior_probabilities::StateProbabilities) -Associate each node with array of conditional probabilities for each of its states occuring in compatible paths given fixed states and prior probability. +Associate each node with array of conditional probabilities for each of its states occuring in compatible paths given + fixed states and prior probability. Fix node and state using their names. # Examples ```julia @@ -173,17 +199,12 @@ function StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy, node node_index = findfirst(j -> j ==node, diagram.Names) state_index = findfirst(j -> j == state, diagram.States[node_index]) - prior = prior_probabilities.probs[node_index][state_index] - fixed = deepcopy(prior_probabilities.fixed) - - push!(fixed, node_index => state_index) - probs = Dict(i => zeros(diagram.S[i]) for i in 1:length(diagram.S)) - for s in CompatiblePaths(diagram, Z, fixed), i in 1:length(diagram.S) - probs[i][s[i]] += diagram.P(s) / prior #TODO double check that this is correct - end - StateProbabilities(probs, fixed) + return StateProbabilities(diagram, Z, Node(node_index), State(state_index), prior_probabilities) end + + + """ StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy) From 58d8d15c8c1a4a52d8a9f060ef1e8ede207ff884 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 16 Sep 2021 12:45:11 +0300 Subject: [PATCH 104/133] Added test for positive and negative path utility. --- test/influence_diagram.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/influence_diagram.jl b/test/influence_diagram.jl index 453369d7..8333f3ac 100644 --- a/test/influence_diagram.jl +++ b/test/influence_diagram.jl @@ -177,5 +177,11 @@ add_utilities!(diagram, "V", [-1 2 3; 5 6 4]) add_probabilities!(diagram, "A", [0.2, 0.8]) generate_diagram!(diagram) @test diagram.translation == Utility(0) + +@info "Testing positive and negative path utility translations" generate_diagram!(diagram, positive_path_utility=true) @test diagram.translation == Utility(2) +@test all(diagram.U(s, diagram.translation) > 0 for s in paths(diagram.S)) +generate_diagram!(diagram, negative_path_utility=true) +@test diagram.translation == Utility(-7) +@test all(diagram.U(s, diagram.translation) < 0 for s in paths(diagram.S)) From 964c59fbd6c0e0b3ba3656cb93e159d7f1bfadc3 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 16 Sep 2021 13:07:20 +0300 Subject: [PATCH 105/133] Fixed parametrs VaR and CVaR function calls. --- src/printing.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/printing.jl b/src/printing.jl index f28f8707..b62d05ad 100644 --- a/src/printing.jl +++ b/src/printing.jl @@ -112,9 +112,8 @@ end Print risk measures. """ function print_risk_measures(U_distribution::UtilityDistribution, αs::Vector{Float64}; fmt = "%f") - u, p = U_distribution.u, U_distribution.p - VaR = [value_at_risk(u, p, α) for α in αs] - CVaR = [conditional_value_at_risk(u, p, α) for α in αs] + VaR = [value_at_risk(U_distribution, α) for α in αs] + CVaR = [conditional_value_at_risk(U_distribution, α) for α in αs] df = DataFrame(α = αs, VaR = VaR, CVaR = CVaR) pretty_table(df, formatters = ft_printf(fmt)) end From 154d703b91a9eb57c6f1e99a73ca397039b3df55 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 16 Sep 2021 14:06:26 +0300 Subject: [PATCH 106/133] Fixed tests for decision model, analysis and printing. --- test/decision_model.jl | 103 ++++++++++++++++++++--------------------- 1 file changed, 51 insertions(+), 52 deletions(-) diff --git a/test/decision_model.jl b/test/decision_model.jl index 7a82a782..71a0268f 100644 --- a/test/decision_model.jl +++ b/test/decision_model.jl @@ -3,85 +3,84 @@ using DecisionProgramming function influence_diagram(rng::AbstractRNG, n_C::Int, n_D::Int, n_V::Int, m_C::Int, m_D::Int, states::Vector{Int}, n_inactive::Int) - C, D, V = random_diagram(rng, n_C, n_D, n_V, m_C, m_D) - S = States(rng, states, length(C) + length(D)) - X = [Probabilities(rng, c, S; n_inactive=n_inactive) for c in C] - Y = [Consequences(rng, v, S; low=-1.0, high=1.0) for v in V] - - validate_influence_diagram(S, C, D, V) - - s_c = sortperm([c.j for c in C]) - s_d = sortperm([d.j for d in D]) - s_v = sortperm([v.j for v in V]) - C, D, V = C[s_c], D[s_d], V[s_v] - X, Y = X[s_c], Y[s_v] - P = DefaultPathProbability(C, X) - U = DefaultPathUtility(V, Y) - - return D, S, P, U + diagram = InfluenceDiagram() + random_diagram!(rng, diagram, n_C, n_D, n_V, m_C, m_D, states) + for c in diagram.C + random_probabilities!(rng, diagram, c; n_inactive=n_inactive) + end + for v in diagram.V + random_utilities!(rng, diagram, v; low=-1.0, high=1.0) + end + + # Names needed for printing functions only + diagram.Names = ["node$j" for j in 1:n_C+n_D+n_V] + diagram.States = [["s$s" for s in 1:n_s] for n_s in diagram.S] + + diagram.P = DefaultPathProbability(diagram.C, diagram.I_j[diagram.C], diagram.X) + diagram.U = DefaultPathUtility(diagram.I_j[diagram.V], diagram.Y) + + return diagram end -function test_decision_model(D, S, P, U, n_inactive, probability_scale_factor, probability_cut) +function test_decision_model(diagram, n_inactive, probability_scale_factor, probability_cut) model = Model() @info "Testing DecisionVariables" - z = DecisionVariables(model, S, D) + z = DecisionVariables(model, diagram) @info "Testing PathCompatibilityVariables" - x_s = PathCompatibilityVariables(model, z, S, P; probability_cut = probability_cut) - - @info "Testing PositivePathUtility" - U′ = if probability_cut U else PositivePathUtility(S, U) end + x_s = PathCompatibilityVariables(model, diagram, z; probability_cut = probability_cut) @info "Testing probability_cut" - lazy_probability_cut(model, x_s, P) + lazy_probability_cut(model, diagram, x_s) @info "Testing expected_value" if probability_scale_factor > 0 - EV = expected_value(model, x_s, U′, P; probability_scale_factor = probability_scale_factor) + EV = expected_value(model, diagram, x_s; probability_scale_factor = probability_scale_factor) else - @test_throws DomainError expected_value(model, x_s, U′, P; probability_scale_factor = probability_scale_factor) + @test_throws DomainError expected_value(model, diagram, x_s; probability_scale_factor = probability_scale_factor) end @info "Testing conditional_value_at_risk" if probability_scale_factor > 0 - CVaR = conditional_value_at_risk(model, x_s, U′, P, 0.2; probability_scale_factor = probability_scale_factor) + CVaR = conditional_value_at_risk(model, diagram, x_s, 0.2; probability_scale_factor = probability_scale_factor) else - @test_throws DomainError conditional_value_at_risk(model, x_s, U′, P, 0.2; probability_scale_factor = probability_scale_factor) + @test_throws DomainError conditional_value_at_risk(model, diagram, x_s, 0.2; probability_scale_factor = probability_scale_factor) end @test true end -function test_analysis_and_printing(D, S, P, U) +function test_analysis_and_printing(diagram) @info("Creating random decision strategy") - Z_j = [LocalDecisionStrategy(rng, d, S) for d in D] - Z = DecisionStrategy(D, Z_j) + Z_j = [LocalDecisionStrategy(rng, diagram, d) for d in diagram.D] + Z = DecisionStrategy(diagram.D, diagram.I_j[diagram.D], Z_j) @info "Testing CompatiblePaths" - @test all(true for s in CompatiblePaths(S, P.C, Z)) - @test_throws DomainError CompatiblePaths(S, P.C, Z, Dict(D[1].j => 1)) - node, state = (P.C[1].j, 1) - @test all(s[node] == state for s in CompatiblePaths(S, P.C, Z, Dict(node => state))) + @test all(true for s in CompatiblePaths(diagram, Z)) + @test_throws DomainError CompatiblePaths(diagram, Z, Dict(diagram.D[1] => State(1))) + node, state = (diagram.C[1], State(1)) + @test all(s[node] == state for s in CompatiblePaths(diagram, Z, Dict(node => state))) @info "Testing UtilityDistribution" - udist = UtilityDistribution(S, P, U, Z) + U_distribution = UtilityDistribution(diagram, Z) @info "Testing StateProbabilities" - sprobs = StateProbabilities(S, P, Z) + S_probabilities = StateProbabilities(diagram, Z) @info "Testing conditional StateProbabilities" - sprobs2 = StateProbabilities(S, P, Z, node, state, sprobs) + S_probabilities2 = StateProbabilities(diagram, Z, node, state, S_probabilities) @info "Testing " - print_decision_strategy(S, Z) - print_utility_distribution(udist) - print_state_probabilities(sprobs, [c.j for c in P.C]) - print_state_probabilities(sprobs, [d.j for d in D]) - print_state_probabilities(sprobs2, [c.j for c in P.C]) - print_state_probabilities(sprobs2, [d.j for d in D]) - print_statistics(udist) - print_risk_measures(udist, [0.0, 0.05, 0.1, 0.2, 1.0]) + print_decision_strategy(diagram, Z, S_probabilities) + print_decision_strategy(diagram, Z, S_probabilities, show_incompatible_states=true) + print_utility_distribution(U_distribution) + print_state_probabilities(diagram, S_probabilities, [diagram.Names[c] for c in diagram.C]) + print_state_probabilities(diagram, S_probabilities, [diagram.Names[d] for d in diagram.D]) + print_state_probabilities(diagram, S_probabilities2, [diagram.Names[c] for c in diagram.C]) + print_state_probabilities(diagram, S_probabilities2, [diagram.Names[d] for d in diagram.D]) + print_statistics(U_distribution) + print_risk_measures(U_distribution, [0.0, 0.05, 0.1, 0.2, 1.0]) @test true end @@ -89,13 +88,13 @@ end @info "Testing model construction" rng = MersenneTwister(4) for (n_C, n_D, states, n_inactive, probability_scale_factor, probability_cut) in [ - (3, 2, [1, 2, 3], 0, 1.0, true), - (3, 2, [1, 2, 3], 0, -1.0, true), + (3, 2, [2, 3, 4], 0, 1.0, true), + (3, 2, [2, 3], 0, -1.0, true), (3, 2, [3], 1, 100.0, true), - (3, 2, [1, 2, 3], 0, -1.0, false), - (3, 2, [3], 1, 10.0, false) + (3, 2, [2, 3], 0, -1.0, false), + (3, 2, [4], 1, 10.0, false) ] - D, S, P, U = influence_diagram(rng, n_C, n_D, 2, 2, 2, states, n_inactive) - test_decision_model(D, S, P, U, n_inactive, probability_scale_factor, probability_cut) - test_analysis_and_printing(D, S, P, U) + diagram = influence_diagram(rng, n_C, n_D, 2, 2, 2, states, n_inactive) + test_decision_model(diagram, n_inactive, probability_scale_factor, probability_cut) + test_analysis_and_printing(diagram) end From 0c7a70206dbb1f8ef84785e83676cf38f443de69 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 16 Sep 2021 14:58:50 +0300 Subject: [PATCH 107/133] Improved docstring and added checks for probabilities only being added for chance nodes and utilities only for value nodes. --- src/random.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/random.jl b/src/random.jl index 0e3f6289..7e5d94c6 100644 --- a/src/random.jl +++ b/src/random.jl @@ -40,6 +40,7 @@ Parameter `m_C` and `m_D` are the upper bounds for the size of the information s - `rng::AbstractRNG`: Random number generator. - `n_C::Int`: Number of chance nodes. - `n_D::Int`: Number of decision nodes. +- `n_V::Int`: Number of value nodes. - `m_C::Int`: Upper bound for size of information set for chance nodes. - `m_D::Int`: Upper bound for size of information set for decision nodes. - `states::Vector{State}`: The number of states for each chance and decision node @@ -115,6 +116,9 @@ Probabilities!(rng, diagram, c) ``` """ function random_probabilities!(rng::AbstractRNG, diagram::InfluenceDiagram, c::Node; n_inactive::Int=0) + if !(c in diagram.C) + throw(DomainError("Probabilities can only be added for chance nodes.")) + end I_c = diagram.I_j[c] states = diagram.S[I_c] state = diagram.S[c] @@ -169,6 +173,9 @@ Utilities!(rng, diagram, v) ``` """ function random_utilities!(rng::AbstractRNG, diagram::InfluenceDiagram, v::Node; low::Float64=-1.0, high::Float64=1.0) + if !(v in diagram.V) + throw(DomainError("Utilities can only be added for value nodes.")) + end if !(high > low) throw(DomainError("high should be greater than low")) end From bcf487ec47c483634245edcf2c79c3b96495b960 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 16 Sep 2021 14:59:51 +0300 Subject: [PATCH 108/133] Fixed tests for random.jl. --- test/random.jl | 80 +++++++++++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/test/random.jl b/test/random.jl index 29c31be2..e87e9275 100644 --- a/test/random.jl +++ b/test/random.jl @@ -4,44 +4,58 @@ using DecisionProgramming rng = MersenneTwister(4) @info "Testing random_diagram" -@test_throws DomainError random_diagram(rng, -1, 1, 1, 1, 1) -@test_throws DomainError random_diagram(rng, 1, -1, 1, 1, 1) -@test_throws DomainError random_diagram(rng, 0, 0, 1, 1, 1) -@test_throws DomainError random_diagram(rng, 1, 1, 0, 1, 1) -@test_throws DomainError random_diagram(rng, 1, 1, 1, 0, 1) -@test_throws DomainError random_diagram(rng, 1, 1, 1, 1, 0) +diagram = InfluenceDiagram() +@test_throws DomainError random_diagram!(rng, diagram, -1, 1, 1, 1, 1, [2]) +@test_throws DomainError random_diagram!(rng, diagram, 1, -1, 1, 1, 1, [2]) +@test_throws DomainError random_diagram!(rng, diagram, 0, 0, 1, 1, 1, [2]) +@test_throws DomainError random_diagram!(rng, diagram, 1, 1, 0, 1, 1, [2]) +@test_throws DomainError random_diagram!(rng, diagram, 1, 1, 1, 0, 1, [2]) +@test_throws DomainError random_diagram!(rng, diagram, 1, 1, 1, 1, 0, [2]) +@test_throws DomainError random_diagram!(rng, diagram, 1, 1, 1, 1, 1, [1]) for (n_C, n_D) in [(1, 0), (0, 1)] rng = RandomDevice() - C, D, V = random_diagram(rng, n_C, n_D, 1, 1, 1) - @test isa(C, Vector{ChanceNode}) - @test isa(D, Vector{DecisionNode}) - @test isa(V, Vector{ValueNode}) - @test length(C) == n_C - @test length(D) == n_D - @test length(V) == 1 - @test all(!isempty(v.I_j) for v in V) + diagram = InfluenceDiagram() + random_diagram!(rng, diagram, n_C, n_D, 1, 1, 1, [2]) + @test isa(diagram.C, Vector{Node}) + @test isa(diagram.D, Vector{Node}) + @test isa(diagram.V, Vector{Node}) + @test length(diagram.C) == n_C + @test length(diagram.D) == n_D + @test length(diagram.V) == 1 + @test all(!isempty(I_v) for I_v in diagram.I_j[diagram.V]) + @test isa(diagram.S, States) end -@info "Testing random States" -@test_throws DomainError States(rng, [0], 10) -@test isa(States(rng, [2, 3], 10), States) - @info "Testing random Probabilities" -S = States([2, 3, 2]) -c = ChanceNode(3, [1, 2]) -@test isa(Probabilities(rng, c, S; n_inactive=0), Probabilities) -@test isa(Probabilities(rng, c, S; n_inactive=1), Probabilities) -@test isa(Probabilities(rng, c, S; n_inactive=2*3*(2-1)), Probabilities) -@test_throws DomainError Probabilities(rng, c, S; n_inactive=2*3*(2-1)+1) - -@info "Testing random Consequences" -S = States([2, 3]) -v = ValueNode(3, [1, 2]) -@test isa(Consequences(rng, v, S; low=-1.0, high=1.0), Consequences) -@test_throws DomainError Consequences(rng, v, S; low=1.1, high=1.0) +diagram = InfluenceDiagram() +random_diagram!(rng, diagram, 2, 2, 1, 1, 1, [2]) +@test_throws DomainError random_probabilities!(rng, diagram, diagram.D[1]; n_inactive=0) +random_probabilities!(rng, diagram, diagram.C[1]; n_inactive=0) +@test isa(diagram.X[1], Probabilities) +random_probabilities!(rng, diagram, diagram.C[2]; n_inactive=1) +@test isa(diagram.X[2], Probabilities) + +diagram = InfluenceDiagram() +diagram.C = Node[1,3] +diagram.D = Node[2] +diagram.I_j = [Node[], Node[], Node[1,2]] +diagram.S = States(State[2, 3, 2]) +diagram.X = Vector{Probabilities}(undef, 2) +random_probabilities!(rng, diagram, diagram.C[2]; n_inactive=2*3*(2-1)) +@test isa(diagram.X[2], Probabilities) +@test_throws DomainError random_probabilities!(rng, diagram, diagram.C[2]; n_inactive=2*3*(2-1)+1) + + +@info "Testing random Utilities" +diagram = InfluenceDiagram() +random_diagram!(rng, diagram, 1, 1, 2, 1, 1, [2]) +random_utilities!(rng, diagram, diagram.V[1], low=-1.0, high=1.0) +@test isa(diagram.Y[1], Utilities) +@test_throws DomainError random_utilities!(rng, diagram, diagram.V[1], low=1.1, high=1.0) +@test_throws DomainError random_utilities!(rng, diagram, diagram.C[1], low=-1.0, high=1.0) @info "Testing random LocalDecisionStrategy" -S = States([2, 3, 2]) -d = DecisionNode(3, [1, 2]) -@test isa(LocalDecisionStrategy(rng, d, S), LocalDecisionStrategy) +diagram = InfluenceDiagram() +random_diagram!(rng, diagram, 2, 2, 2, 2, 2, [2]) +@test isa(LocalDecisionStrategy(rng, diagram, diagram.D[1]), LocalDecisionStrategy) From c0d047eb3b9a388bb05f3ed42ff13a8c8fefda57 Mon Sep 17 00:00:00 2001 From: Helmi Hankimaa Date: Thu, 16 Sep 2021 15:01:02 +0300 Subject: [PATCH 109/133] Added logging info. --- test/decision_model.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/decision_model.jl b/test/decision_model.jl index 71a0268f..e6149972 100644 --- a/test/decision_model.jl +++ b/test/decision_model.jl @@ -71,7 +71,7 @@ function test_analysis_and_printing(diagram) @info "Testing conditional StateProbabilities" S_probabilities2 = StateProbabilities(diagram, Z, node, state, S_probabilities) - @info "Testing " + @info "Testing printing functions" print_decision_strategy(diagram, Z, S_probabilities) print_decision_strategy(diagram, Z, S_probabilities, show_incompatible_states=true) print_utility_distribution(U_distribution) From d760c7b252ca9c7d289f760a5cedef8285b04088 Mon Sep 17 00:00:00 2001 From: solliolli Date: Tue, 28 Sep 2021 15:37:05 +0300 Subject: [PATCH 110/133] Added two helper functions --- src/DecisionProgramming.jl | 2 ++ src/influence_diagram.jl | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/DecisionProgramming.jl b/src/DecisionProgramming.jl index 4dcc6ba5..3d4d96ca 100644 --- a/src/DecisionProgramming.jl +++ b/src/DecisionProgramming.jl @@ -29,6 +29,8 @@ export Node, InfluenceDiagram, generate_arcs!, generate_diagram!, + index_of, + num_states, add_node!, ProbabilityMatrix, set_probability!, diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 56a95357..26336b6b 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -765,7 +765,7 @@ function validate_structure(Nodes::Vector{AbstractNode}, C_and_D::Vector{Abstrac if n_CD == 0 throw(DomainError("The influence diagram must have chance or decision nodes.")) end - if !(union((n.I_j for n in Nodes)...) ⊆ Set(n.name for n in Nodes)) + if !(union((n.I_j for n in Nodes)...) ⊆ Set(n.name for n in Nodes)) throw(DomainError("Each node that is part of an information set should be added as a node.")) end # Checking the information sets of C and D nodes @@ -942,6 +942,38 @@ function generate_diagram!(diagram::InfluenceDiagram; end +""" + function index_of(diagram::InfluenceDiagram, node::Name) + +Get the index of a given node. + +# Example +```julia +julia> idx_O = index_of(diagram, "O") +``` +""" +function index_of(diagram::InfluenceDiagram, node::Name) + idx = findfirst(isequal(node), diagram.Names) + if isnothing(idx) + throw(DomainError("Name $node not found in the diagram.")) + end + return idx +end + +""" + function num_states(diagram::InfluenceDiagram, node::Name) + +Get the number of states in a given node. + +# Example +```julia +julia> NS_O = num_states(diagram, "O") +``` +""" +function num_states(diagram::InfluenceDiagram, node::Name) + return diagram.S[index_of(diagram, node)] +end + # --- ForbiddenPath and FixedPath outer construction functions --- """ function ForbiddenPath(diagram::InfluenceDiagram, nodes::Vector{Name}, paths::Vector{NTuple{N, Name}}) where N From 015f3dd72e8efbdded7ec43add8ecdd81fda8de2 Mon Sep 17 00:00:00 2001 From: solliolli Date: Tue, 28 Sep 2021 15:37:23 +0300 Subject: [PATCH 111/133] Finished updating the CPP example --- .../contingent-portfolio-programming.md | 166 ++++++++---------- examples/contingent-portfolio-programming.jl | 52 +++--- 2 files changed, 97 insertions(+), 121 deletions(-) diff --git a/docs/src/examples/contingent-portfolio-programming.md b/docs/src/examples/contingent-portfolio-programming.md index c7944a28..10c9e931 100644 --- a/docs/src/examples/contingent-portfolio-programming.md +++ b/docs/src/examples/contingent-portfolio-programming.md @@ -30,33 +30,14 @@ using DecisionProgramming Random.seed!(42) -const dᴾ = 1 # Decision node: range for number of patents -const cᵀ = 2 # Chance node: technical competitiveness -const dᴬ = 3 # Decision node: range for number of applications -const cᴹ = 4 # Chance node: market share -const DP_states = ["0-3 patents", "3-6 patents", "6-9 patents"] -const CT_states = ["low", "medium", "high"] -const DA_states = ["0-5 applications", "5-10 applications", "10-15 applications"] -const CM_states = ["low", "medium", "high"] - -S = States([ - (length(DP_states), [dᴾ]), - (length(CT_states), [cᵀ]), - (length(DA_states), [dᴬ]), - (length(CM_states), [cᴹ]), -]) -C = Vector{ChanceNode}() -D = Vector{DecisionNode}() -V = Vector{ValueNode}() -X = Vector{Probabilities}() -Y = Vector{Consequences}() -``` +diagram = InfluenceDiagram() -### Decision on range of number of patents +add_node!(diagram, DecisionNode("DP", [], ["0-3 patents", "3-6 patents", "6-9 patents"])) +add_node!(diagram, ChanceNode("CT", ["DP"], ["low", "medium", "high"])) +add_node!(diagram, DecisionNode("DA", ["DP", "CT"], ["0-5 applications", "5-10 applications", "10-15 applications"])) +add_node!(diagram, ChanceNode("CM", ["CT", "DA"], ["low", "medium", "high"])) -```julia -I_DP = Vector{Node}() -push!(D, DecisionNode(dᴾ, I_DP)) +generate_arcs!(diagram) ``` ### Technical competitiveness probability @@ -64,20 +45,11 @@ push!(D, DecisionNode(dᴾ, I_DP)) Probability of technical competitiveness $c_j^T$ given the range $d_i^P$: $ℙ(c_j^T∣d_i^P)∈[0,1]$. A high number of patents increases probability of high competitiveness and a low number correspondingly increases the probability of low competitiveness. ```julia -I_CT = [dᴾ] -X_CT = zeros(S[dᴾ], S[cᵀ]) +X_CT = ProbabilityMatrix(diagram, "CT") X_CT[1, :] = [1/2, 1/3, 1/6] X_CT[2, :] = [1/3, 1/3, 1/3] X_CT[3, :] = [1/6, 1/3, 1/2] -push!(C, ChanceNode(cᵀ, I_CT)) -push!(X, Probabilities(cᵀ, X_CT)) -``` - -### Decision on range of number of applications - -```julia -I_DA = [dᴾ, cᵀ] -push!(D, DecisionNode(dᴬ, I_DA)) +add_probabilities!(diagram, "CT", X_CT) ``` ### Market share probability @@ -85,8 +57,7 @@ push!(D, DecisionNode(dᴬ, I_DA)) Probability of market share $c_l^M$ given the technical competitiveness $c_j^T$ and range $d_k^A$: $ℙ(c_l^M∣c_j^T,d_k^A)∈[0,1]$. Higher competitiveness and number of application projects both increase the probability of high market share. ```julia -I_CM = [cᵀ, dᴬ] -X_CM = zeros(S[cᵀ], S[dᴬ], S[cᴹ]) +X_CM = ProbabilityMatrix(diagram, "CM") X_CM[1, 1, :] = [2/3, 1/4, 1/12] X_CM[1, 2, :] = [1/2, 1/3, 1/6] X_CM[1, 3, :] = [1/3, 1/3, 1/3] @@ -96,26 +67,14 @@ X_CM[2, 3, :] = [1/6, 1/3, 1/2] X_CM[3, 1, :] = [1/3, 1/3, 1/3] X_CM[3, 2, :] = [1/6, 1/3, 1/2] X_CM[3, 3, :] = [1/12, 1/4, 2/3] -push!(C, ChanceNode(cᴹ, I_CM)) -push!(X, Probabilities(cᴹ, X_CM)) -``` - -We add a dummy value node to avoid problems with the influence diagram validation. Without this, the final chance node would be seen as redundant. - -```julia -push!(V, ValueNode(5, [cᴹ])) -push!(Y,Consequences(5, zeros(S[cᴹ]))) +add_probabilities!(diagram, "CM", X_CM) ``` -### Validating the Influence Diagram +### Generating the Influence Diagram +We are going to be using a custom objective function, and don't need the default path utilities for that. ```julia -validate_influence_diagram(S, C, D, V) -sort!.((C, D, V, X, Y), by = x -> x.j) -``` - -```julia -P = DefaultPathProbability(C, X) +generate_diagram!(diagram, default_utility=false) ``` ## Decision Model: Portfolio Selection @@ -123,7 +82,7 @@ P = DefaultPathProbability(C, X) We create the decision variables $z(s_j|s_{I(j)})$ and notice that the activation of paths that are compatible with the decision strategy is handled by the problem specific variables and constraints together with the custom objective function, eliminating the need for separate variables representing path activation. ```julia model = Model() -z = DecisionVariables(model, S, D) +z = DecisionVariables(model, diagram) ``` ### Creating problem specific variables @@ -136,6 +95,13 @@ Technology project $t$ costs $I_t∈ℝ^+$ and generates $O_t∈ℕ$ patents. Application project $a$ costs $I_a∈ℝ^+$ and generates $O_a∈ℕ$ applications. If completed, provides cash flow $V(a|c_l^M)∈ℝ^+.$ ```julia + +# Number of states in each node +n_DP = num_states(diagram, "DP") +n_CT = num_states(diagram, "CT") +n_DA = num_states(diagram, "DA") +n_CM = num_states(diagram, "CM") + n_T = 5 # number of technology projects n_A = 5 # number of application projects I_t = rand(n_T)*0.1 # costs of technology projects @@ -143,7 +109,7 @@ O_t = rand(1:3,n_T) # number of patents for each tech project I_a = rand(n_T)*2 # costs of application projects O_a = rand(2:4,n_T) # number of applications for each appl. project -V_A = rand(S[cᴹ], n_A).+0.5 # Value of an application +V_A = rand(n_CM, n_A).+0.5 # Value of an application V_A[1, :] .+= -0.5 # Low market share: less value V_A[3, :] .+= 0.5 # High market share: more value ``` @@ -153,8 +119,8 @@ Decision variables $x^T(t)∈\{0, 1\}$ indicate which technologies are selected. Decision variables $x^A(a∣d_i^P,c_j^T)∈\{0, 1\}$ indicate which applications are selected. ```julia -x_T = variables(model, [S[dᴾ]...,n_T]; binary=true) -x_A = variables(model, [S[dᴾ]...,S[cᵀ]...,S[dᴬ]..., n_A]; binary=true) +x_T = variables(model, [n_DP, n_T]; binary=true) +x_A = variables(model, [n_DP, n_CT, n_DA, n_A]; binary=true) ``` Number of patents $x^T(t) = ∑_i x_i^T(t) z(d_i^P)$ @@ -191,36 +157,36 @@ z_dA = z.z[2] $$∑_t x_i^T(t) \le z(d_i^P)n_T, \quad \forall i$$ ```julia -@constraint(model, [i=1:3], +@constraint(model, [i=1:n_DP], sum(x_T[i,t] for t in 1:n_T) <= z_dP[i]*n_T) ``` $$∑_a x_k^A(a∣d_i^P,c_j^T) \le z(d_i^P)n_A, \quad \forall i,j,k$$ ```julia -@constraint(model, [i=1:3, j=1:3, k=1:3], +@constraint(model, [i=1:n_DP, j=1:n_CT, k=1:n_DA], sum(x_A[i,j,k,a] for a in 1:n_A) <= z_dP[i]*n_A) ``` $$∑_a x_k^A(a∣d_i^P,c_j^T) \le z(d_k^A|d_i^P,c_j^T)n_A, \quad \forall i,j,k$$ ```julia -@constraint(model, [i=1:3, j=1:3, k=1:3], +@constraint(model, [i=1:n_DP, j=1:n_CT, k=1:n_DA], sum(x_A[i,j,k,a] for a in 1:n_A) <= z_dA[i,j,k]*n_A) ``` $$q_i^P - (1-z(d_i^P))M \le \sum_t x_i^T(t)O_t \le q_{i+1}^P + (1-z(d_i^P))M - \varepsilon, \quad \forall i$$ ```julia -@constraint(model, [i=1:3], +@constraint(model, [i=1:n_DP], q_P[i] - (1 - z_dP[i])*M <= sum(x_T[i,t]*O_t[t] for t in 1:n_T)) -@constraint(model, [i=1:3], +@constraint(model, [i=1:n_DP], sum(x_T[i,t]*O_t[t] for t in 1:n_T) <= q_P[i+1] + (1 - z_dP[i])*M - ε) ``` $$q_k^A - (1-z(d_k^A|d_i^P,c_j^T))M \le \sum_a x_k^A(a∣d_i^P,c_j^T)O_a \le q_{k+1}^A + (1-z(d_k^A|d_i^P,c_j^T))M - \varepsilon, \quad \forall i,j,k$$ ```julia -@constraint(model, [i=1:3, j=1:3, k=1:3], +@constraint(model, [i=1:n_DP, j=1:n_CT, k=1:n_DA], q_A[k] - (1 - z_dA[i,j,k])*M <= sum(x_A[i,j,k,a]*O_a[a] for a in 1:n_A)) -@constraint(model, [i=1:3, j=1:3, k=1:3], +@constraint(model, [i=1:n_DP, j=1:n_CT, k=1:n_DA], sum(x_A[i,j,k,a]*O_a[a] for a in 1:n_A) <= q_A[k+1] + (1 - z_dA[i,j,k])*M - ε) ``` @@ -231,9 +197,9 @@ $$x_k^A(a∣d_i^P,c_j^T) \le x_i^T(t), \quad \forall i,j,k$$ As an example, we state that application projects 1 and 2 require technology project 1, and application project 2 also requires technology project 2. ```julia -@constraint(model, [i=1:3, j=1:3, k=1:3], x_A[i,j,k,1] <= x_T[i,1]) -@constraint(model, [i=1:3, j=1:3, k=1:3], x_A[i,j,k,2] <= x_T[i,1]) -@constraint(model, [i=1:3, j=1:3, k=1:3], x_A[i,j,k,2] <= x_T[i,2]) +@constraint(model, [i=1:n_DP, j=1:n_CT, k=1:n_DA], x_A[i,j,k,1] <= x_T[i,1]) +@constraint(model, [i=1:n_DP, j=1:n_CT, k=1:n_DA], x_A[i,j,k,2] <= x_T[i,1]) +@constraint(model, [i=1:n_DP, j=1:n_CT, k=1:n_DA], x_A[i,j,k,2] <= x_T[i,2]) ``` $$x_i^T(t)∈\{0, 1\}, \quad \forall i$$ @@ -250,10 +216,11 @@ However, using the expected value objective would lead to a quadratic objective $$\sum_i \left\{ \sum_{j,k,l} p(c_j^T \mid d_i^P) p(c_l^M \mid c_j^T, d_k^A) \left[\sum_a x_k^A(a \mid d_i^P,c_j^T) (V(a \mid c_l^M) - I_a)\right] - \sum_t x_i^T(t) I_t \right\}$$ ```julia -patent_investment_cost = @expression(model, [i=1:S[1]], sum(x_T[i, t] * I_t[t] for t in 1:n_T)) -application_investment_cost = @expression(model, [i=1:S[1], j=1:S[2], k=1:S[3]], sum(x_A[i, j, k, a] * I_a[a] for a in 1:n_A)) -application_value = @expression(model, [i=1:S[1], j=1:S[2], k=1:S[3], l=1:S[4]], sum(x_A[i, j, k, a] * V_A[l, a] for a in 1:n_A)) -@objective(model, Max, sum( sum( P((i,j,k,l)) * (application_value[i,j,k,l] - application_investment_cost[i,j,k]) for j in 1:S[2], k in 1:S[3], l in 1:S[4] ) - patent_investment_cost[i] for i in 1:S[1] )) +patent_investment_cost = @expression(model, [i=1:n_DP], sum(x_T[i, t] * I_t[t] for t in 1:n_T)) +application_investment_cost = @expression(model, [i=1:n_DP, j=1:n_CT, k=1:n_DA], sum(x_A[i, j, k, a] * I_a[a] for a in 1:n_A)) +application_value = @expression(model, [i=1:n_DP, j=1:n_CT, k=1:n_DA, l=1:n_CM], sum(x_A[i, j, k, a] * V_A[l, a] for a in 1:n_A)) +@objective(model, Max, sum( sum( diagram.P(convert.(State, (i,j,k,l))) * (application_value[i,j,k,l] - application_investment_cost[i,j,k]) for j in 1:n_CT, k in 1:n_DA, l in 1:n_CM ) - patent_investment_cost[i] for i in 1:n_DP )) + ``` @@ -275,36 +242,47 @@ The optimal decision strategy and the utility distribution are printed. The stra ```julia Z = DecisionStrategy(z) +S_probabilities = StateProbabilities(diagram, Z) ``` ```julia-repl -julia> print_decision_strategy(S, Z) -┌────────┬────┬───┐ -│ Nodes │ () │ 1 │ -├────────┼────┼───┤ -│ States │ () │ 3 │ -└────────┴────┴───┘ -┌────────┬────────┬───┐ -│ Nodes │ (1, 2) │ 3 │ -├────────┼────────┼───┤ -│ States │ (1, 1) │ 1 │ -│ States │ (2, 1) │ 1 │ -│ States │ (3, 1) │ 3 │ -│ States │ (1, 2) │ 1 │ -│ States │ (2, 2) │ 1 │ -│ States │ (3, 2) │ 3 │ -│ States │ (1, 3) │ 1 │ -│ States │ (2, 3) │ 1 │ -│ States │ (3, 3) │ 3 │ -└────────┴────────┴───┘ +julia> print_decision_strategy(diagram, Z, S_probabilities) +┌────────────────┐ +│ Decision in DP │ +├────────────────┤ +│ 6-9 patents │ +└────────────────┘ +┌─────────────────────┬────────────────────┐ +│ State(s) of DP, CT │ Decision in DA │ +├─────────────────────┼────────────────────┤ +│ 6-9 patents, low │ 10-15 applications │ +│ 6-9 patents, medium │ 10-15 applications │ +│ 6-9 patents, high │ 10-15 applications │ +└─────────────────────┴────────────────────┘ +``` + +We use a custom path utility function to obtain the utility distribution. + +```julia +struct PathUtility <: AbstractPathUtility + data::Array{AffExpr} +end +Base.getindex(U::PathUtility, i::State) = getindex(U.data, i) +Base.getindex(U::PathUtility, I::Vararg{State,N}) where N = getindex(U.data, I...) +(U::PathUtility)(s::Path) = value.(U[s...]) + +path_utility = [@expression(model, + sum(x_A[s[index_of(diagram, "DP")], s[index_of(diagram, "CT")], s[index_of(diagram, "DA")], a] * (V_A[s[index_of(diagram, "CM")], a] - I_a[a]) for a in 1:n_A) - + sum(x_T[s[index_of(diagram, "DP")], t] * I_t[t] for t in 1:n_T)) for s in paths(diagram.S)] +diagram.U = PathUtility(path_utility) ``` ```julia -udist = UtilityDistribution(S, P, U, Z) +U_distribution = UtilityDistribution(diagram, Z) ``` ```julia-repl -julia> print_utility_distribution(udist) +julia> print_utility_distribution(U_distribution) ┌───────────┬─────────────┐ │ Utility │ Probability │ │ Float64 │ Float64 │ @@ -316,7 +294,7 @@ julia> print_utility_distribution(udist) ``` ```julia-repl -julia> print_statistics(udist) +julia> print_statistics(U_distribution) ┌──────────┬────────────┐ │ Name │ Statistics │ │ String │ Float64 │ diff --git a/examples/contingent-portfolio-programming.jl b/examples/contingent-portfolio-programming.jl index 26f39b19..13489680 100644 --- a/examples/contingent-portfolio-programming.jl +++ b/examples/contingent-portfolio-programming.jl @@ -4,14 +4,6 @@ using DecisionProgramming Random.seed!(42) -function num_states(diagram::InfluenceDiagram, node::Name) - idx = findfirst(isequal(node), diagram.Names) - if isnothing(idx) - throw(DomainError("Name $node not found in the diagram.")) - end - return diagram.S[idx] -end - @info("Creating the influence diagram.") diagram = InfluenceDiagram() @@ -58,6 +50,12 @@ function variables(model::Model, dims::AbstractVector{Int}; binary::Bool=false) return v end +# Number of states in each node +n_DP = num_states(diagram, "DP") +n_CT = num_states(diagram, "CT") +n_DA = num_states(diagram, "DA") +n_CM = num_states(diagram, "CM") + n_T = 5 # number of technology projects n_A = 5 # number of application projects I_t = rand(n_T)*0.1 # costs of technology projects @@ -65,12 +63,12 @@ O_t = rand(1:3,n_T) # number of patents for each tech project I_a = rand(n_T)*2 # costs of application projects O_a = rand(2:4,n_T) # number of applications for each appl. project -V_A = rand(num_states(diagram, "CM"), n_A).+0.5 # Value of an application +V_A = rand(n_CM, n_A).+0.5 # Value of an application V_A[1, :] .+= -0.5 # Low market share: less value V_A[3, :] .+= 0.5 # High market share: more value -x_T = variables(model, [num_states(diagram, "DP"), n_T]; binary=true) -x_A = variables(model, [num_states(diagram, "DP"), num_states(diagram, "CT"), num_states(diagram, "DA"), n_A]; binary=true) +x_T = variables(model, [n_DP, n_T]; binary=true) +x_A = variables(model, [n_DP, n_CT, n_DA, n_A]; binary=true) M = 20 # a large constant ε = 0.5*minimum([O_t O_a]) # a helper variable, allows using ≤ instead of < in constraints (28b) and (29b) @@ -80,35 +78,35 @@ q_A = [0, 5, 10, 15] # limits of the application intervals z_dP = z.z[1] z_dA = z.z[2] -@constraint(model, [i=1:3], +@constraint(model, [i=1:n_DP], sum(x_T[i,t] for t in 1:n_T) <= z_dP[i]*n_T) #(25) -@constraint(model, [i=1:3, j=1:3, k=1:3], +@constraint(model, [i=1:n_DP, j=1:n_CT, k=1:n_DA], sum(x_A[i,j,k,a] for a in 1:n_A) <= z_dP[i]*n_A) #(26) -@constraint(model, [i=1:3, j=1:3, k=1:3], +@constraint(model, [i=1:n_DP, j=1:n_CT, k=1:n_DA], sum(x_A[i,j,k,a] for a in 1:n_A) <= z_dA[i,j,k]*n_A) #(27) #(28a) -@constraint(model, [i=1:3], +@constraint(model, [i=1:n_DP], q_P[i] - (1 - z_dP[i])*M <= sum(x_T[i,t]*O_t[t] for t in 1:n_T)) #(28b) -@constraint(model, [i=1:3], +@constraint(model, [i=1:n_DP], sum(x_T[i,t]*O_t[t] for t in 1:n_T) <= q_P[i+1] + (1 - z_dP[i])*M - ε) #(29a) -@constraint(model, [i=1:3, j=1:3, k=1:3], +@constraint(model, [i=1:n_DP, j=1:n_CT, k=1:n_DA], q_A[k] - (1 - z_dA[i,j,k])*M <= sum(x_A[i,j,k,a]*O_a[a] for a in 1:n_A)) #(29b) -@constraint(model, [i=1:3, j=1:3, k=1:3], +@constraint(model, [i=1:n_DP, j=1:n_CT, k=1:n_DA], sum(x_A[i,j,k,a]*O_a[a] for a in 1:n_A) <= q_A[k+1] + (1 - z_dA[i,j,k])*M - ε) -@constraint(model, [i=1:3, j=1:3, k=1:3], x_A[i,j,k,1] <= x_T[i,1]) -@constraint(model, [i=1:3, j=1:3, k=1:3], x_A[i,j,k,2] <= x_T[i,1]) -@constraint(model, [i=1:3, j=1:3, k=1:3], x_A[i,j,k,2] <= x_T[i,2]) +@constraint(model, [i=1:n_DP, j=1:n_CT, k=1:n_DA], x_A[i,j,k,1] <= x_T[i,1]) +@constraint(model, [i=1:n_DP, j=1:n_CT, k=1:n_DA], x_A[i,j,k,2] <= x_T[i,1]) +@constraint(model, [i=1:n_DP, j=1:n_CT, k=1:n_DA], x_A[i,j,k,2] <= x_T[i,2]) @info("Creating model objective.") -patent_investment_cost = @expression(model, [i=1:diagram.S[1]], sum(x_T[i, t] * I_t[t] for t in 1:n_T)) -application_investment_cost = @expression(model, [i=1:diagram.S[1], j=1:diagram.S[2], k=1:diagram.S[3]], sum(x_A[i, j, k, a] * I_a[a] for a in 1:n_A)) -application_value = @expression(model, [i=1:diagram.S[1], j=1:diagram.S[2], k=1:diagram.S[3], l=1:diagram.S[4]], sum(x_A[i, j, k, a] * V_A[l, a] for a in 1:n_A)) -@objective(model, Max, sum( sum( diagram.P((i,j,k,l)) * (application_value[i,j,k,l] - application_investment_cost[i,j,k]) for j in State(1):diagram.S[2], k in State(1):diagram.S[3], l in State(1):diagram.S[4] ) - patent_investment_cost[i] for i in State(1):diagram.S[1] )) +patent_investment_cost = @expression(model, [i=1:n_DP], sum(x_T[i, t] * I_t[t] for t in 1:n_T)) +application_investment_cost = @expression(model, [i=1:n_DP, j=1:n_CT, k=1:n_DA], sum(x_A[i, j, k, a] * I_a[a] for a in 1:n_A)) +application_value = @expression(model, [i=1:n_DP, j=1:n_CT, k=1:n_DA, l=1:n_CM], sum(x_A[i, j, k, a] * V_A[l, a] for a in 1:n_A)) +@objective(model, Max, sum( sum( diagram.P(convert.(State, (i,j,k,l))) * (application_value[i,j,k,l] - application_investment_cost[i,j,k]) for j in 1:n_CT, k in 1:n_DA, l in 1:n_CM ) - patent_investment_cost[i] for i in 1:n_DP )) @info("Starting the optimization process.") @@ -137,8 +135,8 @@ Base.getindex(U::PathUtility, I::Vararg{State,N}) where N = getindex(U.data, I.. (U::PathUtility)(s::Path) = value.(U[s...]) path_utility = [@expression(model, - sum(x_A[s[1:3]..., a] * (V_A[s[4], a] - I_a[a]) for a in 1:n_A) - - sum(x_T[s[1], t] * I_t[t] for t in 1:n_T)) for s in paths(diagram.S)] + sum(x_A[s[index_of(diagram, "DP")], s[index_of(diagram, "CT")], s[index_of(diagram, "DA")], a] * (V_A[s[index_of(diagram, "CM")], a] - I_a[a]) for a in 1:n_A) - + sum(x_T[s[index_of(diagram, "DP")], t] * I_t[t] for t in 1:n_T)) for s in paths(diagram.S)] diagram.U = PathUtility(path_utility) @info("Computing utility distribution.") From 2b2c0c195eab63edfd5e89fefd390087db18e698 Mon Sep 17 00:00:00 2001 From: solliolli Date: Wed, 29 Sep 2021 14:09:32 +0300 Subject: [PATCH 112/133] fixing broken docstrings --- docs/src/api.md | 18 ++++++++++++------ src/influence_diagram.jl | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index 002d88cf..db2f76f9 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -1,6 +1,7 @@ # API Reference `DecisionProgramming.jl` API reference. +include("influence_diagram.jl") ## `influence_diagram.jl` ### Nodes @@ -48,6 +49,7 @@ DefaultPathUtility ``` ### InfluenceDiagram +```@docs InfluenceDiagram generate_arcs! generate_diagram! @@ -58,6 +60,9 @@ add_probabilities! UtilityMatrix set_utility! add_utilities! +index_of +num_states +``` ### Decision Strategy ```@docs @@ -71,7 +76,7 @@ DecisionStrategy ```@docs DecisionVariables PathCompatibilityVariables -lazy_probability_cut(::Model, ::PathCompatibilityVariables, ::AbstractPathProbability) +lazy_probability_cut ``` ### Objective Functions @@ -110,9 +115,10 @@ print_risk_measures ## `random.jl` ```@docs -random_diagram(::AbstractRNG, ::Int, ::Int, ::Int, ::Int, ::Int) -States(::AbstractRNG, ::Vector{State}, ::Int) -Probabilities(::AbstractRNG, ::ChanceNode, ::States; ::Int) -Consequences(::AbstractRNG, ::ValueNode, ::States; ::Float64, ::Float64) -LocalDecisionStrategy(::AbstractRNG, ::DecisionNode, ::States) +information_set(::AbstractRNG, ::Node, ::Int) +information_set(::AbstractRNG, ::Vector{Node}, ::Int) +random_diagram! +random_probabilities! +random_utilities! +LocalDecisionStrategy(::AbstractRNG, ::InfluenceDiagram, ::Node) ``` diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 26336b6b..b734aad5 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -24,7 +24,11 @@ Node type for directed, acyclic graph. """ abstract type AbstractNode end +""" + struct ChanceNode <: AbstractNode +A struct for chance nodes, includes the name, information set and states of the node +""" struct ChanceNode <: AbstractNode name::Name I_j::Vector{Name} @@ -35,6 +39,11 @@ struct ChanceNode <: AbstractNode end +""" + struct DecisionNode <: AbstractNode + +A struct for decision nodes, includes the name, information set and states of the node +""" struct DecisionNode <: AbstractNode name::Name I_j::Vector{Name} @@ -44,6 +53,11 @@ struct DecisionNode <: AbstractNode end end +""" + struct ValueNode <: AbstractNode + +A struct for value nodes, includes the name and information set of the node +""" struct ValueNode <: AbstractNode name::Name I_j::Vector{Name} From e62eb46b9355ffb7d3cceaf940aa661dcac00100 Mon Sep 17 00:00:00 2001 From: solliolli Date: Fri, 1 Oct 2021 09:35:48 +0300 Subject: [PATCH 113/133] Finished fixing documentation --- docs/src/api.md | 8 +++----- docs/src/examples/pig-breeding.md | 4 ++-- src/random.jl | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index db2f76f9..0ac63505 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -1,8 +1,6 @@ # API Reference `DecisionProgramming.jl` API reference. -include("influence_diagram.jl") - ## `influence_diagram.jl` ### Nodes ```@docs @@ -22,7 +20,7 @@ Path ForbiddenPath FixedPath paths(::AbstractVector{State}) -paths(::AbstractVector{State}, FixedPath) +paths(::AbstractVector{State}, ::FixedPath) ``` ### Probabilities @@ -115,10 +113,10 @@ print_risk_measures ## `random.jl` ```@docs -information_set(::AbstractRNG, ::Node, ::Int) -information_set(::AbstractRNG, ::Vector{Node}, ::Int) random_diagram! random_probabilities! random_utilities! LocalDecisionStrategy(::AbstractRNG, ::InfluenceDiagram, ::Node) +DecisionProgramming.information_set(::AbstractRNG, ::Node, ::Int64) +DecisionProgramming.information_set(::AbstractRNG, ::Vector{Node}, ::Int64) ``` diff --git a/docs/src/examples/pig-breeding.md b/docs/src/examples/pig-breeding.md index 29f8282c..c886318c 100644 --- a/docs/src/examples/pig-breeding.md +++ b/docs/src/examples/pig-breeding.md @@ -158,7 +158,7 @@ add_utilities!(diagram, "MP", [300.0, 1000.0]) ### Generate influence diagram After adding nodes, generating arcs and defining probability and utility values, we generate the full influence diagram. By default this function uses the default path probabilities and utilities, which are defined as the joint probability of all chance events in the diagram and the sum of utilities in value nodes, respectively. In the [Contingent Portfolio Programming](contingent-portfolio-programming.md) example, we show how to use a user-defined custom path utility function. -In the pig breeding problem, when the $N$ is large some of the path utilities become negative. In this case, we choose to use the [positive path utility](../decision-programming/decision_model.md) transformation, which allows us to exclude the probability cut in the next section. +In the pig breeding problem, when the $N$ is large some of the path utilities become negative. In this case, we choose to use the [positive path utility](../decision-programming/decision-model.md) transformation, which allows us to exclude the probability cut in the next section. ```julia generate_diagram!(diagram, positive_path_utility = true) @@ -166,7 +166,7 @@ generate_diagram!(diagram, positive_path_utility = true) ## Decision model -Next we initialise the JuMP model and add the decision variables. Then we add the path compatibility variables. Since we applied an affine transformation to the utility function, making all path utilities positive, the probability cut can be excluded from the model. The purpose of this is discussed in the [theoretical section](../decision-programming/decision-model.md) of this documentation. +Next we initialise the JuMP model and add the decision variables. Then we add the path compatibility variables. Since we applied an affine transformation to the utility function, making all path utilities positive, the probability cut can be excluded from the model. The purpose of this is discussed in the [theoretical section](../decision-programming/decision-model.md) of this documentation. ```julia model = Model() diff --git a/src/random.jl b/src/random.jl index 7e5d94c6..176da143 100644 --- a/src/random.jl +++ b/src/random.jl @@ -1,7 +1,7 @@ using Random """ - function information_set(rng::AbstractRNG, j::Int, n_I::Int) + function information_set(rng::AbstractRNG, j::Node, n_I::Int) Generates random information sets for chance and decision nodes. """ From fc6f7bae514bec1c5e93c19d9472f988f5e920e4 Mon Sep 17 00:00:00 2001 From: solliolli Date: Fri, 1 Oct 2021 11:13:06 +0300 Subject: [PATCH 114/133] Fixed deprecated syntax --- src/printing.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/printing.jl b/src/printing.jl index b62d05ad..aaabd30f 100644 --- a/src/printing.jl +++ b/src/printing.jl @@ -32,10 +32,10 @@ function print_decision_strategy(diagram::InfluenceDiagram, Z::DecisionStrategy, if !show_incompatible_states filter!(row -> row.decisions != "--", df) end - pretty_table(df, ["State(s) of $(join([diagram.Names[i] for i in I_d], ", "))", "Decision in $(diagram.Names[d])"], alignment=:l) + pretty_table(df, header = ["State(s) of $(join([diagram.Names[i] for i in I_d], ", "))", "Decision in $(diagram.Names[d])"], alignment=:l) else df = DataFrame(decisions = diagram.States[d][s_d]) - pretty_table(df, ["Decision in $(diagram.Names[d])"], alignment=:l) + pretty_table(df, header = ["Decision in $(diagram.Names[d])"], alignment=:l) end end end From 6dc99649de1f16f8c657478b15002374ab909bf4 Mon Sep 17 00:00:00 2001 From: solliolli Date: Fri, 1 Oct 2021 12:18:59 +0300 Subject: [PATCH 115/133] Cleaner syntax for setting values of probability and utility matrices --- docs/src/api.md | 2 - docs/src/examples/CHD_preventative_care.md | 40 ++--- docs/src/examples/n-monitoring.md | 12 +- docs/src/examples/pig-breeding.md | 16 +- docs/src/examples/used-car-buyer.md | 34 ++--- docs/src/usage.md | 22 ++- examples/CHD_preventative_care.jl | 38 +++-- examples/n_monitoring.jl | 8 +- examples/pig_breeding.jl | 16 +- examples/used_car_buyer.jl | 32 ++-- src/DecisionProgramming.jl | 2 - src/influence_diagram.jl | 164 ++++++++------------- 12 files changed, 165 insertions(+), 221 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index 0ac63505..20edc536 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -53,10 +53,8 @@ generate_arcs! generate_diagram! add_node! ProbabilityMatrix -set_probability! add_probabilities! UtilityMatrix -set_utility! add_utilities! index_of num_states diff --git a/docs/src/examples/CHD_preventative_care.md b/docs/src/examples/CHD_preventative_care.md index dc6d7509..b94f394e 100644 --- a/docs/src/examples/CHD_preventative_care.md +++ b/docs/src/examples/CHD_preventative_care.md @@ -53,7 +53,7 @@ const T_states = ["TRS", "GRS", "no test"] const TD_states = ["treatment", "no treatment"] const R_states = [string(x) * "%" for x in [0:1:100;]] ``` - + We then add the nodes. The chance and decision nodes are identified by their names. When declaring the nodes, they are also given information sets and states. Notice that nodes $R0$ and $H$ are root nodes, meaning that their information sets are empty. In Decision Programming, we add the chance and decision nodes in the follwoing way. ```julia add_node!(diagram, ChanceNode("R0", [], R_states)) @@ -74,7 +74,7 @@ add_node!(diagram, ValueNode("HB", ["H", "TD"])) ``` ### Generate arcs -Now that all of the nodes have been added to the influence diagram we generate the arcs between the nodes. This step automatically orders the nodes, gives them indices and reorganises the information into the appropriate form. +Now that all of the nodes have been added to the influence diagram we generate the arcs between the nodes. This step automatically orders the nodes, gives them indices and reorganises the information into the appropriate form. ```julia generate_arcs!(diagram) ``` @@ -90,7 +90,7 @@ $$ℙ(R0 \neq 12\%)= 0. $$ The probability matrix of node $R0$ is added in the following way. Remember that the `ProbabilityMatrix` function initialises the matrix with zeros. ```julia X_R0 = ProbabilityMatrix(diagram, "R0") -set_probability!(X_R0, [chosen_risk_level], 1) +X_R0[chosen_risk_level] = 1 add_probabilities!(diagram, "R0", X_R0) ``` @@ -104,11 +104,11 @@ $$ℙ(H = \text{no CHD} | R0 = \alpha) = 1 - \alpha$$ Since node $R0$ is deterministic and the health node $H$ is defined in this way, in our model the patient has a 12% chance of experiencing a CHD event and 88% chance of remaining healthy. -In Decision Programming the probability matrix of node $H$ has dimensions (101, 2) because its information set consisting of node $R0$ has 101 states and node $H$ has 2 states. We first set the column related to the state $CHD$ with values from `data.risk_levels` which are $0.00, 0.01, ..., 0.99, 1.00$ and the other column as its complement event. +In this Decision Programming model, the probability matrix of node $H$ has dimensions (101, 2) because its information set consisting of node $R0$ has 101 states and node $H$ has 2 states. We first set the column related to the state $CHD$ with values from `data.risk_levels` which are $0.00, 0.01, ..., 0.99, 1.00$ and the other column as its complement event. ```julia X_H = ProbabilityMatrix(diagram, "H") -set_probability!(X_H, [:, "CHD"], data.risk_levels) -set_probability!(X_H, [:, "no CHD"], 1 .- data.risk_levels) +X_H[:, "CHD"] = data.risk_levels +X_H[:, "no CHD"] = 1 .- data.risk_levels add_probabilities!(diagram, "H", X_H) ``` @@ -146,15 +146,15 @@ cost_GRS = -0.004 forbidden = 0 #the cost of forbidden test combinations is negligible Y_TC = UtilityMatrix(diagram, "TC") -set_utility!(Y_TC, ["TRS", "TRS"], forbidden) -set_utility!(Y_TC, ["TRS", "GRS"], cost_TRS + cost_GRS) -set_utility!(Y_TC, ["TRS", "no test"], cost_TRS) -set_utility!(Y_TC, ["GRS", "TRS"], cost_TRS + cost_GRS) -set_utility!(Y_TC, ["GRS", "GRS"], forbidden) -set_utility!(Y_TC, ["GRS", "no test"], cost_GRS) -set_utility!(Y_TC, ["no test", "TRS"], cost_TRS) -set_utility!(Y_TC, ["no test", "GRS"], cost_GRS) -set_utility!(Y_TC, ["no test", "no test"], 0) +Y_TC["TRS", "TRS"] = forbidden +Y_TC["TRS", "GRS"] = cost_TRS + cost_GRS +Y_TC["TRS", "no test"] = cost_TRS +Y_TC["GRS", "TRS"] = cost_TRS + cost_GRS +Y_TC["GRS", "GRS"] = forbidden +Y_TC["GRS", "no test"] = cost_GRS +Y_TC["no test", "TRS"] = cost_TRS +Y_TC["no test", "GRS"] = cost_GRS +Y_TC["no test", "no test"] = 0 add_utilities!(diagram, "TC", Y_TC) ``` @@ -163,10 +163,10 @@ The health benefits that are achieved are determined by whether treatment is adm ```julia Y_HB = UtilityMatrix(diagram, "HB") -set_utility!(Y_HB, ["CHD", "treatment"], 6.89713671259061) -set_utility!(Y_HB, ["CHD", "no treatment"], 6.65436854256236 ) -set_utility!(Y_HB, ["no CHD", "treatment"], 7.64528451705134) -set_utility!(Y_HB, ["no CHD", "no treatment"], 7.70088349200034) +Y_HB["CHD", "treatment"] = 6.89713671259061 +Y_HB["CHD", "no treatment"] = 6.65436854256236 +Y_HB["no CHD", "treatment"] = 7.64528451705134 +Y_HB["no CHD", "no treatment"] = 7.70088349200034 add_utilities!(diagram, "HB", Y_HB) ``` @@ -196,7 +196,7 @@ fixed_R0 = FixedPath(diagram, Dict("R0" => chosen_risk_level)) We also choose a scale factor of 10000, which will be used to scale the path probabilities. The probabilities need to be scaled because in this specific problem they are very small since the $R$ nodes have a large number of states. Scaling the probabilities helps the solver find an optimal solution. -We then declare the path compatibility variables. We fix the state of the deterministic $R0$ node , forbid the unwanted testing strategies and scale the probabilities by giving them as parameters in the function call. +We then declare the path compatibility variables. We fix the state of the deterministic $R0$ node , forbid the unwanted testing strategies and scale the probabilities by giving them as parameters in the function call. ```julia scale_factor = 10000.0 diff --git a/docs/src/examples/n-monitoring.md b/docs/src/examples/n-monitoring.md index 86d10bf3..25118df3 100644 --- a/docs/src/examples/n-monitoring.md +++ b/docs/src/examples/n-monitoring.md @@ -93,10 +93,10 @@ In Decision Programming we add these probabilities by declaring probabilty matri for i in 1:N x, y = rand(2) X_R = ProbabilityMatrix(diagram, "R$i") - set_probability!(X_R, ["high", "high"], max(x, 1-x)) - set_probability!(X_R, ["high", "low"], 1-max(x, 1-x)) - set_probability!(X_R, ["low", "low"], max(y, 1-y)) - set_probability!(X_R, ["low", "high"], 1-max(y, 1-y)) + X_R["high", "high"] = max(x_R, 1-x_R) + X_R["high", "low"] = 1 - max(x_R, 1-x_R) + X_R["low", "low"] = max(y_R, 1-y_R) + X_R["low", "high"] = 1-max(y_R, 1-y_R) add_probabilities!(diagram, "R$i", X_R) end ``` @@ -108,7 +108,7 @@ $$ℙ(F=failure∣A_N,...,A_1,L=high)=\frac{\max{\{x, 1-x\}}}{\exp{(b ∑_{k=1,. $$ℙ(F=failure∣A_N,...,A_1,L=low)=\frac{\min{\{y, 1-y\}}}{\exp{(b ∑_{k=1,...,N} f(A_k))}}$$ -First we initialise the probability matrix for node $F$. +First we initialise the probability matrix for node $F$. ```julia X_F = ProbabilityMatrix(diagram, "F") ``` @@ -155,7 +155,7 @@ We first declare the utility matrix for node $T$. ```julia Y_T = UtilityMatrix(diagram, "T") ``` -This matrix has dimensions $(2, \textcolor{orange}{2, 2, 2, 2})$, where the dimensions correspond to the numbers of states the nodes in the information set have. Similarly as before, the first dimension corresponds to the states of node $F$ and the other 4 dimensions (in oragne) correspond to the states of the $A_k$ nodes. The utilities are set and added similarly to how the probabilities were added above. +This matrix has dimensions $(2, \textcolor{orange}{2, 2, 2, 2})$, where the dimensions correspond to the numbers of states the nodes in the information set have. Similarly as before, the first dimension corresponds to the states of node $F$ and the other 4 dimensions (in orange) correspond to the states of the $A_k$ nodes. The utilities are set and added similarly to how the probabilities were added above. ```julia for s in paths([State(2) for i in 1:N]) diff --git a/docs/src/examples/pig-breeding.md b/docs/src/examples/pig-breeding.md index c886318c..2b8d22ee 100644 --- a/docs/src/examples/pig-breeding.md +++ b/docs/src/examples/pig-breeding.md @@ -96,10 +96,10 @@ $$ℙ(h_k = ill ∣ h_{k-1} = ill, \ d_{k-1} = treat)=0.5.$$ In Decision Programming, the probability matrix is define in the following way. Notice, that the ordering of the information state corresponds to the order in which the information set was defined when adding the health nodes. ```julia X_H = ProbabilityMatrix(diagram, "H2") -set_probability!(X_H, ["healthy", "pass", :], [0.2, 0.8]) -set_probability!(X_H, ["healthy", "treat", :], [0.1, 0.9]) -set_probability!(X_H, ["ill", "pass", :], [0.9, 0.1]) -set_probability!(X_H, ["ill", "treat", :], [0.5, 0.5]) +X_H["healthy", "pass", :] = [0.2, 0.8] +X_H["healthy", "treat", :] = [0.1, 0.9] +X_H["ill", "pass", :] = [0.9, 0.1] +X_H["ill", "treat", :] = [0.5, 0.5] ``` Next we define the probability matrix for the test results. Here again, we note that the probability distributions for all test results are identical, and thus we only define the probability matrix once. For the probabilities that the test indicates a pig's health correctly at month $k=1,...,N-1$, we have @@ -112,10 +112,10 @@ In Decision Programming: ```julia X_T = ProbabilityMatrix(diagram, "T1") -set_probability!(X_T, ["ill", "positive"], 0.8) -set_probability!(X_T, ["ill", "negative"], 0.2) -set_probability!(X_T, ["healthy", "negative"], 0.9) -set_probability!(X_T, ["healthy", "positive"], 0.1) +X_T["ill", "positive"] = 0.8 +X_T["ill", "negative"] = 0.2 +X_T["healthy", "negative"] = 0.9 +X_T["healthy", "positive"] = 0.1 ``` We add the probability matrices into the influence diagram using a for-loop. diff --git a/docs/src/examples/used-car-buyer.md b/docs/src/examples/used-car-buyer.md index 2880c7ec..631a2a95 100644 --- a/docs/src/examples/used-car-buyer.md +++ b/docs/src/examples/used-car-buyer.md @@ -69,55 +69,55 @@ generate_arcs!(diagram) ``` ### Probabilities -We continue by defining probability distributions for each chance node. +We continue by defining probability distributions for each chance node. Node $O$ is a root node and has two states thus, its probability distribution is simply defined over the two states. We can use the `ProbabilityMatrix` structure in creating the probability matrix easily without having to worry about the matrix dimensions. We then set the probability values and add the probabililty matrix to the influence diagram. ```julia X_O = ProbabilityMatrix(diagram, "O") -set_probability!(X_O, ["peach"], 0.8) -set_probability!(X_O, ["lemon"], 0.2) +X_O["peach"] = 0.8 +X_O["lemon"] = 0.2 add_probabilities!(diagram, "O", X_O) ``` Node $R$ has two nodes in its information set and three states. The probabilities $P(s_j \mid s_{I(j)})$ must thus be defined for all combinations of states in $O$, $T$ and $R$. We declare the probability distribution over the states of node $R$ for each information state in the following way. More information on defining probability matrices can be found on the [usage page](../usage.md). ```julia X_R = ProbabilityMatrix(diagram, "R") -set_probability!(X_R, ["lemon", "no test", :], [1,0,0]) -set_probability!(X_R, ["lemon", "test", :], [0,1,0]) -set_probability!(X_R, ["peach", "no test", :], [1,0,0]) -set_probability!(X_R, ["peach", "test", :], [0,0,1]) +X_R["lemon", "no test", :] = [1,0,0] +X_R["lemon", "test", :] = [0,1,0] +X_R["peach", "no test", :] = [1,0,0] +X_R["peach", "test", :] = [0,0,1] add_probabilities!(diagram, "R", X_R) ``` -### Utilities +### Utilities We continue by defining the utilities associated with the information states of the value nodes. The utilities $Y_j(𝐬_{I(j)})$ are defined and added similarly to the probabilities. Value node $V1$ has only node $T$ in its information set and node $T$ only has two states. Therefore, the utility matrix of node $V1$ should hold utility values corresponding to states $test$ and $no \ test$. ```julia Y_V1 = UtilityMatrix(diagram, "V1") -set_utility!(Y_V1, ["test"], -25) -set_utility!(Y_V1, ["no test"], 0) +Y_V1["test"] = -25 +Y_V1["no test"] = 0 add_utilities!(diagram, "V1", Y_V1) ``` We then define the utilities associated with the base profit of the purchase in different scenarios. ```julia Y_V2 = UtilityMatrix(diagram, "V2") -set_utility!(Y_V2, ["buy without guarantee"], 100) -set_utility!(Y_V2, ["buy with guarantee"], 40) -set_utility!(Y_V2, ["don't buy"], 0) +Y_V2["buy without guarantee"] = 100 +Y_V2["buy with guarantee"] = 40 +Y_V2["don't buy"] = 0 add_utilities!(diagram, "V2", Y_V2) ``` Finally, we define the utilities corresponding to the repair costs. The rows of the utilities matrix `Y_V3` correspond to the state of the car, while the columns correspond to the decision made in node $A$. Notice that the utility values for the second row are added as a vector, in this case it is important to give the utility values in the correct order. The order of the columns is determined by the order in which the states are given when declaring node $A$. See the [usage page](../usage.md) for more information on the syntax. ```julia Y_V3 = UtilityMatrix(diagram, "V3") -set_utility!(Y_V3, ["lemon", "buy without guarantee"], -200) -set_utility!(Y_V3, ["lemon", "buy with guarantee"], 0) -set_utility!(Y_V3, ["lemon", "don't buy"], 0) -set_utility!(Y_V3, ["peach", :], [-40, -20, 0]) +Y_V3["lemon", "buy without guarantee"] = -200 +Y_V3["lemon", "buy with guarantee"] = 0 +Y_V3["lemon", "don't buy"] = 0 +Y_V3["peach", :] = [-40, -20, 0] add_utilities!(diagram, "V3", Y_V3) ``` diff --git a/docs/src/usage.md b/docs/src/usage.md index 46d84713..b3796537 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -72,7 +72,7 @@ julia> diagram.V ``` ## Probability Matrices -Each chance node needs a probability matrix which describes the probability distribution over its states given an information state. It holds probability values +Each chance node needs a probability matrix which describes the probability distribution over its states given an information state. It holds probability values $$ℙ(X_j=s_j∣X_{I(j)}=𝐬_{I(j)})$$ for all $s_j \in S_j$ and $𝐬_{I(j)} \in 𝐒_{I(j)}$. @@ -120,7 +120,7 @@ julia> diagram.Names 2 ``` -Therefore, the probability matrix of node C2 should have dimensions $(|S_{D1}|, |\ S_{C1}|, \ |S_{C2}|) = (2, 3, 2)$. The probability matrix can be added by declaring the matrix and then filling in the probability values as shown below. +Therefore, the probability matrix of node C2 should have dimensions $(|S_{D1}|, |\ S_{C1}|, \ |S_{C2}|) = (2, 3, 2)$. The probability matrix can be added by declaring the matrix and then filling in the probability values as shown below. ```julia X_C2 = zeros(2, 3, 2) X_C2[1, 1, 1] = ... @@ -157,16 +157,16 @@ julia> size(X_C2) ``` A matrix of type `ProbabilityMatrix` can be filled using the names of the states. The states must however be given in the correct order, according to the order of the nodes in the information set vector `I_j`. Notice that if we use the `Colon` (`:`) to indicate several elements of the matrix, the probability values have to be given in the correct order of the states in `States`. ```julia -julia> set_probability!(X_C2, ["a", "z", "w"], 0.25) +julia> X_C2["a", "z", "w"] = 0.25 0.25 -julia> set_probability!(X_C2, ["z", "a", "v"], 0.75) +julia> X_C2["z", "a", "v"] = 0.75 ERROR: DomainError with Node D1 does not have a state called z.: -julia> set_probability!(X_C2, ["a", "z", "v"], 0.75) +julia> X_C2["a", "z", "v"] = 0.75 0.75 -julia> set_probability!(X_C2, ["a", "x", :], [0.3, 0.7]) +julia> X_C2["a", "x", :] = [0.3, 0.7] 2-element Array{Float64,1}: 0.3 0.7 @@ -184,7 +184,7 @@ julia> X_C2[1, 1, :] = [0.3, 0.7] 2-element Array{Float64,1}: 0.3 0.7 -```` +``` Now, the probability matrix X_C2 is partially filled. ```julia @@ -234,10 +234,10 @@ julia> Y_V = UtilityMatrix(diagram, "V") Inf Inf -julia> set_utility!(Y_V, ["w"], 400) +julia> Y_V["w"] = 400 400 -julia> set_utility!(Y_V, ["v"], -100) +julia> Y_V["v"] = -100 -100 julia> add_utilities!(diagram, "V", Y_V) @@ -259,7 +259,3 @@ generate_diagram!(diagram) In this function, first, the probability and utility matrices in fields `X` and `Y` are sorted according to the chance and value nodes' indices. Second, the path probability and path utility types are declared and added into fields `P` and `U` respectively. These types define how the path probability $p(𝐬)$ and path utility $\mathcal{U}(𝐬)$ are defined in the model. By default, the function will set them to default path probability and default path utility. See the [influence diagram](decision-programming/influence-diagram.md) for more information on default path probability and utility. - - - - diff --git a/examples/CHD_preventative_care.jl b/examples/CHD_preventative_care.jl index 24870234..f1e38abd 100644 --- a/examples/CHD_preventative_care.jl +++ b/examples/CHD_preventative_care.jl @@ -10,7 +10,7 @@ const chosen_risk_level = "12%" # Reading tests' technical performance data (dummy data in this case) -data = CSV.read("examples/CHD_preventative_care_data.csv", DataFrame) +data = CSV.read("CHD_preventative_care_data.csv", DataFrame) # Bayes posterior risk probabilities calculation function @@ -132,13 +132,13 @@ add_node!(diagram, ValueNode("HB", ["H", "TD"])) generate_arcs!(diagram) X_R0 = ProbabilityMatrix(diagram, "R0") -set_probability!(X_R0, [chosen_risk_level], 1) +X_R0[chosen_risk_level] = 1 add_probabilities!(diagram, "R0", X_R0) X_H = ProbabilityMatrix(diagram, "H") -set_probability!(X_H, [:, "CHD"], data.risk_levels) -set_probability!(X_H, [:, "no CHD"], 1 .- data.risk_levels) +X_H[:, "CHD"] = data.risk_levels +X_H[:, "no CHD"] = 1 .- data.risk_levels add_probabilities!(diagram, "H", X_H) X_R = ProbabilityMatrix(diagram, "R1") @@ -153,22 +153,22 @@ cost_TRS = -0.0034645 cost_GRS = -0.004 forbidden = 0 #the cost of forbidden test combinations is negligible Y_TC = UtilityMatrix(diagram, "TC") -set_utility!(Y_TC, ["TRS", "TRS"], forbidden) -set_utility!(Y_TC, ["TRS", "GRS"], cost_TRS + cost_GRS) -set_utility!(Y_TC, ["TRS", "no test"], cost_TRS) -set_utility!(Y_TC, ["GRS", "TRS"], cost_TRS + cost_GRS) -set_utility!(Y_TC, ["GRS", "GRS"], forbidden) -set_utility!(Y_TC, ["GRS", "no test"], cost_GRS) -set_utility!(Y_TC, ["no test", "TRS"], cost_TRS) -set_utility!(Y_TC, ["no test", "GRS"], cost_GRS) -set_utility!(Y_TC, ["no test", "no test"], 0) +Y_TC["TRS", "TRS"] = forbidden +Y_TC["TRS", "GRS"] = cost_TRS + cost_GRS +Y_TC["TRS", "no test"] = cost_TRS +Y_TC["GRS", "TRS"] = cost_TRS + cost_GRS +Y_TC["GRS", "GRS"] = forbidden +Y_TC["GRS", "no test"] = cost_GRS +Y_TC["no test", "TRS"] = cost_TRS +Y_TC["no test", "GRS"] = cost_GRS +Y_TC["no test", "no test"] = 0 add_utilities!(diagram, "TC", Y_TC) Y_HB = UtilityMatrix(diagram, "HB") -set_utility!(Y_HB, ["CHD", "treatment"], 6.89713671259061) -set_utility!(Y_HB, ["CHD", "no treatment"], 6.65436854256236 ) -set_utility!(Y_HB, ["no CHD", "treatment"], 7.64528451705134) -set_utility!(Y_HB, ["no CHD", "no treatment"], 7.70088349200034) +Y_HB["CHD", "treatment"] = 6.89713671259061 +Y_HB["CHD", "no treatment"] = 6.65436854256236 +Y_HB["no CHD", "treatment"] = 7.64528451705134 +Y_HB["no CHD", "no treatment"] = 7.70088349200034 add_utilities!(diagram, "HB", Y_HB) generate_diagram!(diagram) @@ -194,10 +194,8 @@ optimizer = optimizer_with_attributes( "MIPGap" => 1e-6, ) set_optimizer(model, optimizer) -GC.enable(false) -optimize!(model) -GC.enable(true) +optimize!(model) @info("Extracting results.") Z = DecisionStrategy(z) diff --git a/examples/n_monitoring.jl b/examples/n_monitoring.jl index 64460e1e..364ad8b9 100644 --- a/examples/n_monitoring.jl +++ b/examples/n_monitoring.jl @@ -32,10 +32,10 @@ add_probabilities!(diagram, "L", X_L) for i in 1:N x_R, y_R = rand(2) X_R = ProbabilityMatrix(diagram, "R$i") - set_probability!(X_R, ["high", "high"], max(x_R, 1-x_R)) - set_probability!(X_R, ["high", "low"], 1 - max(x_R, 1-x_R)) - set_probability!(X_R, ["low", "low"], max(y_R, 1-y_R)) - set_probability!(X_R, ["low", "high"], 1-max(y_R, 1-y_R)) + X_R["high", "high"] = max(x_R, 1-x_R) + X_R["high", "low"] = 1 - max(x_R, 1-x_R) + X_R["low", "low"] = max(y_R, 1-y_R) + X_R["low", "high"] = 1-max(y_R, 1-y_R) add_probabilities!(diagram, "R$i", X_R) end diff --git a/examples/pig_breeding.jl b/examples/pig_breeding.jl index ca6629c1..e8acde4d 100644 --- a/examples/pig_breeding.jl +++ b/examples/pig_breeding.jl @@ -27,17 +27,17 @@ add_probabilities!(diagram, "H1", [0.1, 0.9]) # Declare proability matrix for health nodes H_2, ... H_N-1, which have identical information sets and states X_H = ProbabilityMatrix(diagram, "H2") -set_probability!(X_H, ["healthy", "pass", :], [0.2, 0.8]) -set_probability!(X_H, ["healthy", "treat", :], [0.1, 0.9]) -set_probability!(X_H, ["ill", "pass", :], [0.9, 0.1]) -set_probability!(X_H, ["ill", "treat", :], [0.5, 0.5]) +X_H["healthy", "pass", :] = [0.2, 0.8] +X_H["healthy", "treat", :] = [0.1, 0.9] +X_H["ill", "pass", :] = [0.9, 0.1] +X_H["ill", "treat", :] = [0.5, 0.5] # Declare proability matrix for test result nodes T_1...T_N X_T = ProbabilityMatrix(diagram, "T1") -set_probability!(X_T, ["ill", "positive"], 0.8) -set_probability!(X_T, ["ill", "negative"], 0.2) -set_probability!(X_T, ["healthy", "negative"], 0.9) -set_probability!(X_T, ["healthy", "positive"], 0.1) +X_T["ill", "positive"] = 0.8 +X_T["ill", "negative"] = 0.2 +X_T["healthy", "negative"] = 0.9 +X_T["healthy", "positive"] = 0.1 for i in 1:N-1 add_probabilities!(diagram, "T$i", X_T) diff --git a/examples/used_car_buyer.jl b/examples/used_car_buyer.jl index ec188d33..0ad7f140 100644 --- a/examples/used_car_buyer.jl +++ b/examples/used_car_buyer.jl @@ -18,35 +18,33 @@ add_node!(diagram, ValueNode("V3", ["O", "A"])) generate_arcs!(diagram) X_O = ProbabilityMatrix(diagram, "O") -set_probability!(X_O, ["peach"], 0.8) -set_probability!(X_O, ["lemon"], 0.2) +X_O["peach"] = 0.8 +X_O["lemon"] = 0.2 add_probabilities!(diagram, "O", X_O) - X_R = ProbabilityMatrix(diagram, "R") -set_probability!(X_R, ["lemon", "no test", :], [1,0,0]) -set_probability!(X_R, ["lemon", "test", :], [0,1,0]) -set_probability!(X_R, ["peach", "no test", :], [1,0,0]) -set_probability!(X_R, ["peach", "test", :], [0,0,1]) +X_R["lemon", "no test", :] = [1,0,0] +X_R["lemon", "test", :] = [0,1,0] +X_R["peach", "no test", :] = [1,0,0] +X_R["peach", "test", :] = [0,0,1] add_probabilities!(diagram, "R", X_R) Y_V1 = UtilityMatrix(diagram, "V1") -set_utility!(Y_V1, ["test"], -25) -set_utility!(Y_V1, ["no test"], 0) +Y_V1["test"] = -25 +Y_V1["no test"] = 0 add_utilities!(diagram, "V1", Y_V1) - Y_V2 = UtilityMatrix(diagram, "V2") -set_utility!(Y_V2, ["buy without guarantee"], 100) -set_utility!(Y_V2, ["buy with guarantee"], 40) -set_utility!(Y_V2, ["don't buy"], 0) +Y_V2["buy without guarantee"] = 100 +Y_V2["buy with guarantee"] = 40 +Y_V2["don't buy"] = 0 add_utilities!(diagram, "V2", Y_V2) Y_V3 = UtilityMatrix(diagram, "V3") -set_utility!(Y_V3, ["lemon", "buy without guarantee"], -200) -set_utility!(Y_V3, ["lemon", "buy with guarantee"], 0) -set_utility!(Y_V3, ["lemon", "don't buy"], 0) -set_utility!(Y_V3, ["peach", :], [-40, -20, 0]) +Y_V3["lemon", "buy without guarantee"] = -200 +Y_V3["lemon", "buy with guarantee"] = 0 +Y_V3["lemon", "don't buy"] = 0 +Y_V3["peach", :] = [-40, -20, 0] add_utilities!(diagram, "V3", Y_V3) generate_diagram!(diagram) diff --git a/src/DecisionProgramming.jl b/src/DecisionProgramming.jl index 3d4d96ca..3636b2d2 100644 --- a/src/DecisionProgramming.jl +++ b/src/DecisionProgramming.jl @@ -33,10 +33,8 @@ export Node, num_states, add_node!, ProbabilityMatrix, - set_probability!, add_probabilities!, UtilityMatrix, - set_utility!, add_utilities!, LocalDecisionStrategy, DecisionStrategy diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index b734aad5..56617513 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -498,8 +498,36 @@ end Base.size(PM::ProbabilityMatrix) = size(PM.matrix) Base.getindex(PM::ProbabilityMatrix, I::Vararg{Int,N}) where N = getindex(PM.matrix, I...) -Base.setindex!(PM::ProbabilityMatrix, p::T, I::Vararg{Int,N}) where {N, T<:Real} = (PM.matrix[I...] = p) -Base.setindex!(PM::ProbabilityMatrix{N}, X::Array{T}, I::Vararg{Any, N}) where {N, T<:Real} = (PM.matrix[I...] .= X) +function Base.setindex!(PM::ProbabilityMatrix, p::T, I::Vararg{Union{String, Int},N}) where {N, T<:Real} + I2 = [] + for i in 1:N + if isa(I[i], String) + if get(PM.indices[i], I[i], 0) == 0 + throw(DomainError("Node $(probability_matrix.nodes[i]) does not have state $(I[i]).")) + end + push!(I2, PM.indices[i][I[i]]) + else + push!(I2, I[i]) + end + end + PM.matrix[I2...] = p +end +function Base.setindex!(PM::ProbabilityMatrix{N}, P::Array{T}, I::Vararg{Union{String, Int, Colon}, N}) where {N, T<:Real} + I2 = [] + for i in 1:N + if isa(I[i], Colon) + push!(I2, :) + elseif isa(I[i], String) + if get(PM.indices[i], I[i], 0) == 0 + throw(DomainError("Node $(probability_matrix.nodes[i]) does not have state $(I[i]).")) + end + push!(I2, PM.indices[i][I[i]]) + else + push!(I2, I[i]) + end + end + PM.matrix[I2...] = P +end """ function ProbabilityMatrix(diagram::InfluenceDiagram, node::Name) @@ -536,57 +564,6 @@ function ProbabilityMatrix(diagram::InfluenceDiagram, node::Name) return ProbabilityMatrix(names, indices, matrix) end -""" - function set_probability!(probability_matrix::ProbabilityMatrix, scenario::Array{String}, probability::Float64) - -Set a single probability value into probability matrix. - -# Examples -```julia -julia> X_O = ProbabilityMatrix(diagram, "O") -julia> set_probability!(X_O, ["peach"], 0.8) -julia> set_probability!(X_O, ["lemon"], 0.2) -``` -""" -function set_probability!(probability_matrix::ProbabilityMatrix, scenario::Array{String}, probability::Real) - index = Vector{Int}() - for (i, s) in enumerate(scenario) - if get(probability_matrix.indices[i], s, 0) == 0 - throw(DomainError("Node $(probability_matrix.nodes[i]) does not have a state called $s.")) - else - push!(index, get(probability_matrix.indices[i], s, 0)) - end - end - - probability_matrix[index...] = probability -end - -""" - function set_probability!(probability_matrix::ProbabilityMatrix, scenario::Array{Any}, probabilities::Array{Float64}) - -Set multiple probability values into probability matrix. - -# Examples -```julia -julia> X_O = ProbabilityMatrix(diagram, "O") -julia> set_probability!(X_O, ["lemon", "peach"], [0.2, 0.8]) -``` -""" -function set_probability!(probability_matrix::ProbabilityMatrix, scenario::Array{Any}, probabilities::Array{T}) where T<:Real - index = Vector{Any}() - for (i, s) in enumerate(scenario) - if isa(s, Colon) - push!(index, s) - elseif get(probability_matrix.indices[i], s, 0) == 0 - throw(DomainError("Node $(probability_matrix.nodes[i]) does not have state $s.")) - else - push!(index, get(probability_matrix.indices[i], s, 0)) - end - end - - probability_matrix[index...] = probabilities -end - """ function add_probabilities!(diagram::InfluenceDiagram, node::Name, probabilities::AbstractArray{Float64, N}) where N @@ -643,8 +620,36 @@ end Base.size(UM::UtilityMatrix) = size(UM.matrix) Base.getindex(UM::UtilityMatrix, I::Vararg{Int,N}) where N = getindex(UM.matrix, I...) -Base.setindex!(UM::UtilityMatrix, y::T, I::Vararg{Int,N}) where {N, T<:Real} = (UM.matrix[I...] = y) -Base.setindex!(UM::UtilityMatrix{N}, Y::Array{T}, I::Vararg{Any, N}) where {N, T<:Real} = (UM.matrix[I...] .= Y) +function Base.setindex!(UM::UtilityMatrix{N}, y::T, I::Vararg{Union{String, Int},N}) where {N, T<:Real} + I2 = [] + for i in 1:N + if isa(I[i], String) + if get(UM.indices[i], I[i], 0) == 0 + throw(DomainError("Node $(probability_matrix.nodes[i]) does not have state $(I[i]).")) + end + push!(I2, UM.indices[i][I[i]]) + else + push!(I2, I[i]) + end + end + UM.matrix[I2...] = y +end +function Base.setindex!(UM::UtilityMatrix{N}, Y::Array{T}, I::Vararg{Union{String, Int, Colon}, N}) where {N, T<:Real} + I2 = [] + for i in 1:N + if isa(I[i], Colon) + push!(I2, :) + elseif isa(I[i], String) + if get(UM.indices[i], I[i], 0) == 0 + throw(DomainError("Node $(probability_matrix.nodes[i]) does not have state $(I[i]).")) + end + push!(I2, UM.indices[i][I[i]]) + else + push!(I2, I[i]) + end + end + UM.matrix[I2...] = Y +end """ function UtilityMatrix(diagram::InfluenceDiagram, node::Name) @@ -681,55 +686,6 @@ function UtilityMatrix(diagram::InfluenceDiagram, node::Name) return UtilityMatrix(names, indices, matrix) end -""" - function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{String}, utility::Real) - -Set a single utility value into utility matrix. - -# Examples -```julia -julia> Y_V3 = UtilityMatrix(diagram, "V3") -julia> set_utility!(Y_V3, ["lemon", "buy without guarantee"], -200) -``` -""" -function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{String}, utility::Real) - index = Vector{Int}() - for (i, s) in enumerate(scenario) - if get(utility_matrix.indices[i], s, 0) == 0 - throw(DomainError("Node $(utility_matrix.I_v[i]) does not have a state called $s.")) - else - push!(index, get(utility_matrix.indices[i], s, 0)) - end - end - - utility_matrix[index...] = utility -end - -""" - function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{Any}, utility::Array{T}) where T<:Real - -Set multiple utility values into utility matrix. - -# Examples -```julia -julia> Y_V3 = UtilityMatrix(diagram, "V3") -julia> set_utility!(Y_V3, ["peach", :], [-40, -20, 0]) -``` -""" -function set_utility!(utility_matrix::UtilityMatrix, scenario::Array{Any}, utility::Array{T}) where T<:Real - index = Vector{Any}() - for (i, s) in enumerate(scenario) - if isa(s, Colon) - push!(index, s) - elseif get(utility_matrix.indices[i], s, 0) == 0 - throw(DomainError("Node $(utility_matrix.I_v[i]) does not have state $s.")) - else - push!(index, get(utility_matrix.indices[i], s, 0)) - end - end - - utility_matrix[index...] = utility -end """ From 8bd145c2103415aa1ceb8535ef19ad223b642e59 Mon Sep 17 00:00:00 2001 From: solliolli Date: Fri, 1 Oct 2021 12:23:05 +0300 Subject: [PATCH 116/133] Fixed tests --- test/influence_diagram.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/influence_diagram.jl b/test/influence_diagram.jl index 8333f3ac..81755e57 100644 --- a/test/influence_diagram.jl +++ b/test/influence_diagram.jl @@ -137,12 +137,12 @@ generate_arcs!(diagram) @test_throws DomainError ProbabilityMatrix(diagram, "C") @test_throws DomainError ProbabilityMatrix(diagram, "D") X_A = ProbabilityMatrix(diagram, "A") -set_probability!(X_A, ["a"], 0.2) +X_A["a"] = 0.2 @test X_A == [0.2, 0] -set_probability!(X_A, ["b"], 0.9) +X_A["b"] = 0.9 @test X_A == [0.2, 0.9] @test_throws DomainError add_probabilities!(diagram, "A", X_A) -set_probability!(X_A, ["b"], 0.8) +X_A["b"] = 0.8 @test add_probabilities!(diagram, "A", X_A) == [[0.2, 0.8]] @test_throws DomainError add_probabilities!(diagram, "A", X_A) @@ -157,10 +157,10 @@ generate_arcs!(diagram) @test_throws DomainError UtilityMatrix(diagram, "D") Y_V = UtilityMatrix(diagram, "V") @test_throws DomainError add_utilities!(diagram, "V", Y_V) -set_utility!(Y_V, ["a", :], [1, 2, 3]) -set_utility!(Y_V, ["b", "c"], 4) -set_utility!(Y_V, ["b", "a"], 5) -set_utility!(Y_V, ["b", "b"], 6) +Y_V["a", :] = [1, 2, 3] +Y_V["b", "c"] = 4 +Y_V["b", "a"] = 5 +Y_V["b", "b"] = 6 @test Y_V == [1 2 3; 5 6 4] add_utilities!(diagram, "V", Y_V) @test diagram.Y == [[1 2 3; 5 6 4]] From 3cf533c66ee4effe8828cdb469ee774df1c37c27 Mon Sep 17 00:00:00 2001 From: solliolli Date: Thu, 7 Oct 2021 11:05:53 +0300 Subject: [PATCH 117/133] Updates to random diagram generation and documentation --- src/random.jl | 45 ++++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/src/random.jl b/src/random.jl index 176da143..7f6d3737 100644 --- a/src/random.jl +++ b/src/random.jl @@ -31,13 +31,14 @@ end """ - function random_diagram!(rng::AbstractRNG, n_C::Int, n_D::Int, n_V::Int, m_C::Int, m_D::Int, states::Vector{Int}) + random_diagram!(rng::AbstractRNG, diagram::InfluenceDiagram, n_C::Int, n_D::Int, n_V::Int, m_C::Int, m_D::Int, states::Vector{Int}) Generate random decision diagram with `n_C` chance nodes, `n_D` decision nodes, and `n_V` value nodes. Parameter `m_C` and `m_D` are the upper bounds for the size of the information set. # Arguments - `rng::AbstractRNG`: Random number generator. +- `diagram::InfluenceDiagram`: The (empty) influence diagram structure that is filled by this function - `n_C::Int`: Number of chance nodes. - `n_D::Int`: Number of decision nodes. - `n_V::Int`: Number of value nodes. @@ -51,7 +52,7 @@ Parameter `m_C` and `m_D` are the upper bounds for the size of the information s ```julia rng = MersenneTwister(3) diagram = InfluenceDiagram() -random_diagram!(rng, diagram, 5, 2, 3, 2, [2, 4, 5]) +random_diagram!(rng, diagram, 5, 2, 3, 2, 2, [2,3]) ``` """ function random_diagram!(rng::AbstractRNG, diagram::InfluenceDiagram, n_C::Int, n_D::Int, n_V::Int, m_C::Int, m_D::Int, states::Vector{Int}) @@ -98,21 +99,39 @@ function random_diagram!(rng::AbstractRNG, diagram::InfluenceDiagram, n_C::Int, diagram.X = Vector{Probabilities}(undef, n_C) diagram.Y = Vector{Utilities}(undef, n_V) + diagram.Names = ["$(i)" for i in 1:(n+n_V)] + statelist = [] + for i in 1:n + push!(statelist, ["$(j)" for j in 1:diagram.S[i]]) + end + diagram.States = statelist + + for c in diagram.C + random_probabilities!(rng, diagram, c) + end + + for v in diagram.V + random_utilities!(rng, diagram, v) + end + + diagram.P = DefaultPathProbability(diagram.C, diagram.I_j[diagram.C], diagram.X) + diagram.U = DefaultPathUtility(diagram.I_j[diagram.V], diagram.Y) + return diagram end """ - function Probabilities(rng::AbstractRNG, c::ChanceNode, S::States; n_inactive::Int=0) + function random_probabilities!(rng::AbstractRNG, diagram::InfluenceDiagram, c::Node; n_inactive::Int=0) -Generate random probabilities for chance node `c` with `S` states. +Generate random probabilities for chance node `c`. # Examples ```julia rng = MersenneTwister(3) diagram = InfluenceDiagram() -random_diagram!(rng, diagram, 5, 2, 3, 2, [2, 4, 5]) +random_diagram!(rng, diagram, 5, 2, 3, 2, 2, [2,3]) c = diagram.C[1] -Probabilities!(rng, diagram, c) +random_probabilities!(rng, diagram, c) ``` """ function random_probabilities!(rng::AbstractRNG, diagram::InfluenceDiagram, c::Node; n_inactive::Int=0) @@ -159,7 +178,7 @@ end scale(x::Utility, low::Utility, high::Utility) = x * (high - low) + low """ - function Utilities!(rng::AbstractRNG, diagram::InfluenceDiagram, v::Node; low::Float64=-1.0, high::Float64=1.0) + function random_utilities!(rng::AbstractRNG, diagram::InfluenceDiagram, v::Node; low::Float64=-1.0, high::Float64=1.0) Generate random utilities between `low` and `high` for value node `v`. @@ -167,9 +186,9 @@ Generate random utilities between `low` and `high` for value node `v`. ```julia rng = MersenneTwister(3) diagram = InfluenceDiagram() -random_diagram!(rng, diagram, 5, 2, 3, 2, [2, 4, 5]) +random_diagram!(rng, diagram, 5, 2, 3, 2, 2, [2,3]) v = diagram.V[1] -Utilities!(rng, diagram, v) +random_utilities!(rng, diagram, v) ``` """ function random_utilities!(rng::AbstractRNG, diagram::InfluenceDiagram, v::Node; low::Float64=-1.0, high::Float64=1.0) @@ -191,16 +210,16 @@ end """ - function LocalDecisionStrategy(rng::AbstractRNG, d::DecisionNode, S::States) + function LocalDecisionStrategy(rng::AbstractRNG, diagram::InfluenceDiagram, d::Node) Generate random decision strategy for decision node `d`. # Examples ```julia rng = MersenneTwister(3) -d = DecisionNode(2, [1]) -S = States([2, 2]) -LocalDecisionStrategy(rng, d, S) +diagram = InfluenceDiagram() +random_diagram!(rng, diagram, 5, 2, 3, 2, 2, rand(rng, [2,3], 5)) +LocalDecisionStrategy(rng, diagram, diagram.D[1]) ``` """ function LocalDecisionStrategy(rng::AbstractRNG, diagram::InfluenceDiagram, d::Node) From 3f6953f794b26b7914a23615c1b548fb6bc6269c Mon Sep 17 00:00:00 2001 From: solliolli Date: Fri, 8 Oct 2021 09:10:37 +0300 Subject: [PATCH 118/133] Added documentation preview --- docs/make.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index 3f8543ab..0a24ef47 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -33,5 +33,6 @@ makedocs( # See "Hosting Documentation" and deploydocs() in the Documenter manual # for more information. deploydocs( - repo = "github.com/gamma-opt/DecisionProgramming.jl.git" + repo = "github.com/gamma-opt/DecisionProgramming.jl.git", + push_preview = true ) From 9e35766c922ae9c811ed8647417c629f60f75f1c Mon Sep 17 00:00:00 2001 From: solliolli Date: Fri, 8 Oct 2021 15:50:02 +0300 Subject: [PATCH 119/133] Updated docstrings for influence_diagram.jl --- src/influence_diagram.jl | 264 +++++++++++++++++++++++++++------------ 1 file changed, 186 insertions(+), 78 deletions(-) diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 56617513..9482027a 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -82,8 +82,13 @@ const State = Int16 States type. Works like `Vector{State}`. # Examples -```julia -julia> S = States([2, 3, 2, 4]) +```julia-repl +julia> S = States(State.([2, 3, 2, 4])) +4-element States: + 2 + 3 + 2 + 4 ``` """ struct States <: AbstractArray{State, 1} @@ -120,12 +125,17 @@ const Path{N} = NTuple{N, State} where N ForbiddenPath type. # Examples -```julia +```julia-repl julia> ForbiddenPath(([1, 2], Set([(1, 2)]))) +(Int16[1, 2], Set(Tuple{Vararg{Int16,N}} where N[(1, 2)]) + julia> ForbiddenPath[ ([1, 2], Set([(1, 2)])), ([3, 4, 5], Set([(1, 2, 3), (3, 4, 5)])) ] +2-element Array{Tuple{Array{Int16,1},Set{Tuple{Vararg{Int16,N}} where N}},1}: + ([1, 2], Set([(1, 2)])) + ([3, 4, 5], Set([(1, 2, 3), (3, 4, 5)])) ``` """ const ForbiddenPath = Tuple{Vector{Node}, Set{Path}} @@ -137,8 +147,11 @@ const ForbiddenPath = Tuple{Vector{Node}, Set{Path}} FixedPath type. # Examples -```julia +```julia-repl julia> FixedPath(Dict(1=>1, 2=>3)) +Dict{Int16,Int16} with 2 entries: + 2 => 3 + 1 => 1 ``` """ const FixedPath = Dict{Node, State} @@ -151,9 +164,19 @@ Iterate over paths in lexicographical order. # Examples ```julia-repl -julia> states = States([2, 3]) +julia> states = States(State.([2, 3])) +2-element States: + 2 + 3 + julia> vec(collect(paths(states))) -[(1, 1), (2, 1), (1, 2), (2, 2), (1, 3), (2, 3)] +6-element Array{Tuple{Int16,Int16},1}: + (1, 1) + (2, 1) + (1, 2) + (2, 2) + (1, 3) + (2, 3) ``` """ function paths(states::AbstractVector{State}) @@ -167,11 +190,16 @@ Iterate over paths with fixed states in lexicographical order. # Examples ```julia-repl -julia> states = States([2, 3]) -julia> vec(collect(paths(states, Dict(Node(1) => State(2))))) -[(2, 1), (2, 2), (2, 3)] +julia> states = States(State.([2, 3])) +2-element States: + 2 + 3 -julia> vec(collect(paths(states, FixedPath(diagram, Dict("O" => "lemon"))))) +julia> vec(collect(paths(states, Dict(Node(1) => State(2))))) +3-element Array{Tuple{Int16,Int16},1}: + (2, 1) + (2, 2) + (2, 3) ``` """ function paths(states::AbstractVector{State}, fixed::FixedPath) @@ -188,13 +216,23 @@ end """ struct Probabilities{N} <: AbstractArray{Float64, N} -Construct and validate stage probabilities. +Construct and validate stage probabilities (probabilities for a single node). # Examples ```julia-repl julia> data = [0.5 0.5 ; 0.2 0.8] +2×2 Array{Float64,2}: + 0.5 0.5 + 0.2 0.8 + julia> X = Probabilities(Node(2), data) +2×2 Probabilities{2}: + 0.5 0.5 + 0.2 0.8 + julia> s = (1, 2) +(1, 2) + julia> X(s) 0.5 ``` @@ -226,29 +264,36 @@ Base.getindex(P::Probabilities, I::Vararg{Int,N}) where N = getindex(P.data, I.. abstract type AbstractPathProbability end Abstract path probability type. - -# Examples -```julia -julia> struct PathProbability <: AbstractPathProbability - C::Vector{ChanceNode} - # ... -end - -julia> (P::PathProbability)(s::Path) = ... -``` """ abstract type AbstractPathProbability end """ struct DefaultPathProbability <: AbstractPathProbability -Path probability. +Path probability obtained as a product of the probability values corresponding to path s in each chance node. # Examples -```julia -julia> P = DefaultPathProbability(diagram.C, diagram.X) -julia> s = (1, 2) +```julia-repl +julia> C = [2] +1-element Array{Int64,1}: + 2 + +julia> I_j = [[1]] +1-element Array{Array{Int64,1},1}: + [1] + +julia> X = [Probabilities(Node(2), [0.5 0.5; 0.2 0.8])] +1-element Array{Probabilities{2},1}: + [0.5 0.5; 0.2 0.8] + +julia> P = DefaultPathProbability(C, I_j, X) +DefaultPathProbability(Int16[2], Array{Int16,1}[[1]], Probabilities[[0.5 0.5; 0.2 0.8]]) + +julia> s = Path((1, 2)) +(1, 2) + julia> P(s) +0.5 ``` """ struct DefaultPathProbability <: AbstractPathProbability @@ -286,11 +331,21 @@ State utilities. # Examples ```julia-repl -julia> vals = [1.0 -2.0; 3.0 4.0] -julia> Y = Utilities(3, vals) -julia> s = (1, 2) +julia> vals = Utility.([1.0 -2.0; 3.0 4.0]) +2×2 Array{Float32,2}: + 1.0 -2.0 + 3.0 4.0 + +julia> Y = Utilities(Node(3), vals) +2×2 Utilities{2}: + 1.0 -2.0 + 3.0 4.0 + +julia> s = Path((1, 2)) + (1, 2) + julia> Y(s) --2.0 +-2.0f0 ``` """ struct Utilities{N} <: AbstractArray{Utility, N} @@ -318,33 +373,43 @@ Base.getindex(Y::Utilities, I::Vararg{Int,N}) where N = getindex(Y.data, I...) abstract type AbstractPathUtility end Abstract path utility type. - -# Examples -```julia -julia> struct PathUtility <: AbstractPathUtility - V::Vector{ValueNode} - # ... - end - -julia> (U::PathUtility)(s::Path) = ... -julia> (U::PathUtility)(s::Path, translation::Utility) = ... -``` """ abstract type AbstractPathUtility end """ struct DefaultPathUtility <: AbstractPathUtility -Default path utility. +Default path utility obtained as a sum of the utility values corresponding to path s in each value node. # Examples -```julia -julia> U = DefaultPathUtility(V, Y) -julia> s = (1, 2) +```julia-repl +julia> vals = Utility.([1.0 -2.0; 3.0 4.0]) +2×2 Array{Float32,2}: + 1.0 -2.0 + 3.0 4.0 + +julia> Y = [Utilities(Node(3), vals)] +1-element Array{Utilities{2},1}: + [1.0 -2.0; 3.0 4.0] + +julia> I_3 = [[1,2]] +1-element Array{Array{Int64,1},1}: + [1, 2] + +julia> U = DefaultPathUtility(I_3, Y) +DefaultPathUtility(Array{Int16,1}[[1, 2]], Utilities[[1.0 -2.0; 3.0 4.0]]) + +julia> s = Path((1, 2)) +(1, 2) + julia> U(s) +-2.0f0 + +julia> t = Utility(-100.0) + -julia> t = -100.0 julia> U(s, t) +-102.0f0 ``` """ struct DefaultPathUtility <: AbstractPathUtility @@ -404,7 +469,7 @@ Hold all information related to the influence diagram. # Examples ```julia -julia> diagram = InfluenceDiagram() +diagram = InfluenceDiagram() ``` """ mutable struct InfluenceDiagram @@ -466,8 +531,10 @@ end Add node to influence diagram structure. # Examples -```julia +```julia-repl julia> add_node!(diagram, ChanceNode("O", [], ["lemon", "peach"])) +1-element Array{AbstractNode,1}: + ChanceNode("O", String[], ["lemon", "peach"]) ``` """ function add_node!(diagram::InfluenceDiagram, node::AbstractNode) @@ -535,8 +602,11 @@ end Initialise a probability matrix for a given chance node. The matrix is initialised with zeros. # Examples -```julia +```julia-repl julia> X_O = ProbabilityMatrix(diagram, "O") +2-element ProbabilityMatrix{1}: + 0.0 + 0.0 ``` """ function ProbabilityMatrix(diagram::InfluenceDiagram, node::Name) @@ -567,19 +637,30 @@ end """ function add_probabilities!(diagram::InfluenceDiagram, node::Name, probabilities::AbstractArray{Float64, N}) where N -Add probability matrix to influence diagram, specifically to its X vector. +Add probability matrix to influence diagram, specifically to its `X` vector. # Examples ```julia julia> X_O = ProbabilityMatrix(diagram, "O") -julia> set_probability!(X_O, ["lemon", "peach"], [0.2, 0.8]) +2-element ProbabilityMatrix{1}: + 0.0 + 0.0 + +julia> X_O["lemon"] = 0.2 +0.2 + julia> add_probabilities!(diagram, "O", X_O) +printstyled("ERROR: DomainError with Probabilities should sum to one.:"; color=:red) -julia> add_probabilities!(diagram, "O", [0.2, 0.8]) +julia> X_O["peach"] = 0.8 +0.2 -!!! note -The arcs must be generated before probabilities or utilities can be added to the influence diagram. +julia> add_probabilities!(diagram, "O", X_O) +1-element Array{Probabilities,1}: + [0.2, 0.8] ``` +!!! note + The function `generate_arcs!` must be called before probabilities or utilities can be added to the influence diagram. """ function add_probabilities!(diagram::InfluenceDiagram, node::Name, probabilities::AbstractArray{Float64, N}) where N c = findfirst(x -> x==node, diagram.Names) @@ -657,8 +738,11 @@ end Initialise a utility matrix for a value node. The matrix is initialised with `Inf` values. # Examples -```julia +```julia-repl julia> Y_V3 = UtilityMatrix(diagram, "V3") +2×3 UtilityMatrix{2}: + Inf Inf Inf + Inf Inf Inf ``` """ function UtilityMatrix(diagram::InfluenceDiagram, node::Name) @@ -691,19 +775,38 @@ end """ function add_utilities!(diagram::InfluenceDiagram, node::Name, utilities::AbstractArray{T, N}) where {N,T<:Real} -Add utility matrix to influence diagram, specifically to its Y vector. +Add utility matrix to influence diagram, specifically to its `Y` vector. # Examples -```julia +```julia-repl julia> Y_V3 = UtilityMatrix(diagram, "V3") -julia> set_utility!(Y_V3, ["peach", :], [-40, -20, 0]) -julia> set_utility!(Y_V3, ["lemon", :], [-200, 0, 0]) +2×3 UtilityMatrix{2}: + Inf Inf Inf + Inf Inf Inf + +julia> Y_V3["peach", :] = [-40, -20, 0] +3-element Array{Int64,1}: + -40 + -20 + 0 + +julia> Y_V3["lemon", :] = [-200, 0, 0] +3-element Array{Int64,1}: + -200 + 0 + 0 + julia> add_utilities!(diagram, "V3", Y_V3) +1-element Array{Utilities,1}: + [-200.0 0.0 0.0; -40.0 -20.0 0.0] julia> add_utilities!(diagram, "V1", [0, -25]) +2-element Array{Utilities,1}: + [-200.0 0.0 0.0; -40.0 -20.0 0.0] + [0.0, -25.0] ``` !!! note -The arcs must be generated before probabilities or utilities can be added to the influence diagram. + The function `generate_arcs!` must be called before probabilities or utilities can be added to the influence diagram. """ function add_utilities!(diagram::InfluenceDiagram, node::Name, utilities::AbstractArray{T, N}) where {N,T<:Real} v = findfirst(x -> x==node, diagram.Names) @@ -765,7 +868,7 @@ and states are only used in the user interface from here on. # Examples ```julia -julia> generate_arcs!(diagram) +generate_arcs!(diagram) ``` """ function generate_arcs!(diagram::InfluenceDiagram) @@ -869,18 +972,18 @@ Generate complete influence diagram with probabilities and utilities as well. # Examples ```julia -julia> generate_diagram!(diagram) +generate_diagram!(diagram) ``` !!! note -The influence diagram must be generated after probabilities and utilities are added -but before creating the decision model. + The influence diagram must be generated after probabilities and utilities are added + but before creating the decision model. !!! note -If the default probabilities and utilities are not used, define `AbstractPathProbability` -and `AbstractPathUtility` structures and define P(s), U(s) and U(s, t) functions -for them. Add the `AbstractPathProbability` and `AbstractPathUtility` structures -to the influence diagram fields P and U. + If the default probabilities and utilities are not used, define `AbstractPathProbability` + and `AbstractPathUtility` structures and define P(s), U(s) and U(s, t) functions + for them. Add the `AbstractPathProbability` and `AbstractPathUtility` structures + to the influence diagram fields P and U. """ function generate_diagram!(diagram::InfluenceDiagram; default_probability::Bool=true, @@ -918,8 +1021,9 @@ end Get the index of a given node. # Example -```julia +```julia-repl julia> idx_O = index_of(diagram, "O") +1 ``` """ function index_of(diagram::InfluenceDiagram, node::Name) @@ -936,8 +1040,9 @@ end Get the number of states in a given node. # Example -```julia +```julia-repl julia> NS_O = num_states(diagram, "O") +2 ``` """ function num_states(diagram::InfluenceDiagram, node::Name) @@ -957,7 +1062,7 @@ ForbiddenPath outer construction function. Create ForbiddenPath variable. # Example ```julia -julia> ForbiddenPath(diagram, ["R1", "R2"], [("high", "low"), ("low", "high")]) +ForbiddenPath(diagram, ["R1", "R2"], [("high", "low"), ("low", "high")]) ``` """ function ForbiddenPath(diagram::InfluenceDiagram, nodes::Vector{Name}, paths::Vector{NTuple{N, Name}}) where N @@ -997,8 +1102,17 @@ FixedPath outer construction function. Create FixedPath variable. - `fixed::Dict{Name, Name}`: Dictionary of nodes and their fixed states. Order is node=>state, and both are idefied with their names. # Example -```julia -julia> FixedPath(diagram, Dict("R1"=>"high", "R2"=>"high")) +```julia-repl +julia> fixedpath = FixedPath(diagram, Dict("O" => "lemon")) +Dict{Int16,Int16} with 1 entry: + 1 => 1 + +julia> vec(collect(paths(states, fixedpath))) +3-element Array{Tuple{Int16,Int16},1}: + (1, 1) + (1, 2) + (1, 3) + ``` """ function FixedPath(diagram::InfluenceDiagram, fixed::Dict{Name, Name}) @@ -1027,12 +1141,6 @@ end LocalDecisionStrategy{N} <: AbstractArray{Int, N} Local decision strategy type. - -# Examples -```julia -Z = LocalDecisionStrategy(1, data) -Z(s_I) -``` """ struct LocalDecisionStrategy{N} <: AbstractArray{Int, N} d::Node From 9e0f7d2926708d2e45dad70e251dc9d7cab249d5 Mon Sep 17 00:00:00 2001 From: solliolli Date: Fri, 8 Oct 2021 16:23:36 +0300 Subject: [PATCH 120/133] Changes to probability scaling factor and documentation improvements --- src/decision_model.jl | 44 ++++++++++++++++------------------------ src/influence_diagram.jl | 2 +- test/decision_model.jl | 12 +++++------ 3 files changed, 25 insertions(+), 33 deletions(-) diff --git a/src/decision_model.jl b/src/decision_model.jl index 989fdf1e..c9270edc 100644 --- a/src/decision_model.jl +++ b/src/decision_model.jl @@ -34,7 +34,7 @@ Create decision variables and constraints. # Examples ```julia -julia> z = DecisionVariables(model, diagram) +z = DecisionVariables(model, diagram) ``` """ function DecisionVariables(model::Model, diagram::InfluenceDiagram; names::Bool=false, name::String="z") @@ -98,7 +98,8 @@ end name::String="x", forbidden_paths::Vector{ForbiddenPath}=ForbiddenPath[], fixed::FixedPath=Dict{Node, State}(), - probability_cut::Bool=true) + probability_cut::Bool=true, + probability_scale_factor::Float64=1.0) Create path compatibility variables and constraints. @@ -114,10 +115,12 @@ Create path compatibility variables and constraints. - `fixed::FixedPath`: Path compatibility variable will not be generated for paths which do not include these fixed subpaths. - `probability_cut` Includes probability cut constraint in the optimisation model. +- `probability_scale_factor::Float64`: Adjusts conditional value at risk model to + be compatible with the expected value expression if the probabilities were scaled there. # Examples ```julia -julia> x_s = PathCompatibilityVariables(model, diagram; probability_cut = false) +x_s = PathCompatibilityVariables(model, diagram; probability_cut = false) ``` """ function PathCompatibilityVariables(model::Model, @@ -127,7 +130,8 @@ function PathCompatibilityVariables(model::Model, name::String="x", forbidden_paths::Vector{ForbiddenPath}=ForbiddenPath[], fixed::FixedPath=Dict{Node, State}(), - probability_cut::Bool=true) + probability_cut::Bool=true, + probability_scale_factor::Float64=1.0) if !isempty(forbidden_paths) @warn("Forbidden paths is still an experimental feature.") @@ -149,7 +153,7 @@ function PathCompatibilityVariables(model::Model, end if probability_cut - @constraint(model, sum(x * diagram.P(s) for (s, x) in x_s) == 1.0) + @constraint(model, sum(x * diagram.P(s) * probability_scale_factor for (s, x) in x_s) == 1.0 * probability_scale_factor) end x_s @@ -162,7 +166,7 @@ Add a probability cut to the model as a lazy constraint. # Examples ```julia -julia> lazy_probability_cut(model, diagram, x_s) +lazy_probability_cut(model, diagram, x_s) ``` !!! note @@ -188,8 +192,7 @@ end """ expected_value(model::Model, diagram::InfluenceDiagram, - x_s::PathCompatibilityVariables; - probability_scale_factor::Float64=1.0) + x_s::PathCompatibilityVariables) Create an expected value objective. @@ -197,24 +200,17 @@ Create an expected value objective. - `model::Model`: JuMP model into which variables are added. - `diagram::InfluenceDiagram`: Influence diagram structure. - `x_s::PathCompatibilityVariables`: Path compatibility variables. -- `probability_scale_factor::Float64`: Multiplies the path probabilities by this factor. # Examples ```julia -julia> EV = expected_value(model, diagram, x_s) -julia> EV = expected_value(model, diagram, x_s; probability_scale_factor = 10.0) +EV = expected_value(model, diagram, x_s) ``` """ function expected_value(model::Model, diagram::InfluenceDiagram, - x_s::PathCompatibilityVariables; - probability_scale_factor::Float64=1.0) + x_s::PathCompatibilityVariables) - if probability_scale_factor ≤ 0 - throw(DomainError("The probability_scale_factor must be greater than 0.")) - end - - @expression(model, sum(diagram.P(s) * x * diagram.U(s, diagram.translation) * probability_scale_factor for (s, x) in x_s)) + @expression(model, sum(diagram.P(s) * x * diagram.U(s, diagram.translation) for (s, x) in x_s)) end """ @@ -238,9 +234,9 @@ Create a conditional value-at-risk (CVaR) objective. # Examples ```julia -julia> α = 0.05 # Parameter such that 0 ≤ α ≤ 1 -julia> CVaR = conditional_value_at_risk(model, x_s, U, P, α) -julia> CVaR = conditional_value_at_risk(model, x_s, U, P, α; probability_scale_factor = 10.0) +α = 0.05 # Parameter such that 0 ≤ α ≤ 1 +CVaR = conditional_value_at_risk(model, x_s, U, P, α) +CVaR = conditional_value_at_risk(model, x_s, U, P, α; probability_scale_factor = 10.0) ``` """ function conditional_value_at_risk(model::Model, @@ -256,10 +252,6 @@ function conditional_value_at_risk(model::Model, throw(DomainError("α should be 0 < α ≤ 1")) end - if !(probability_scale_factor == 1.0) - @warn("The conditional value at risk is scaled by the probability_scale_factor. Make sure other terms of the objective function are also scaled.") - end - # Pre-computed parameters u = collect(Iterators.flatten(diagram.U(s, diagram.translation) for s in keys(x_s))) u_sorted = sort(u) @@ -296,7 +288,7 @@ function conditional_value_at_risk(model::Model, @constraint(model, sum(values(ρ′_s)) == α * probability_scale_factor) # Return CVaR as an expression - CVaR = @expression(model, sum(ρ_bar * diagram.U(s, diagram.translation) for (s, ρ_bar) in ρ′_s) / α) + CVaR = @expression(model, sum(ρ_bar * diagram.U(s, diagram.translation) for (s, ρ_bar) in ρ′_s) / (α * probability_scale_factor)) return CVaR end diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 9482027a..5e59913d 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -650,7 +650,7 @@ julia> X_O["lemon"] = 0.2 0.2 julia> add_probabilities!(diagram, "O", X_O) -printstyled("ERROR: DomainError with Probabilities should sum to one.:"; color=:red) +ERROR: DomainError with Probabilities should sum to one.: julia> X_O["peach"] = 0.8 0.2 diff --git a/test/decision_model.jl b/test/decision_model.jl index e6149972..d87f5ec8 100644 --- a/test/decision_model.jl +++ b/test/decision_model.jl @@ -29,17 +29,17 @@ function test_decision_model(diagram, n_inactive, probability_scale_factor, prob z = DecisionVariables(model, diagram) @info "Testing PathCompatibilityVariables" - x_s = PathCompatibilityVariables(model, diagram, z; probability_cut = probability_cut) + if probability_scale_factor > 0 + x_s = PathCompatibilityVariables(model, diagram, z; probability_cut = probability_cut, probability_scale_factor = probability_scale_factor) + else + @test_throws DomainError x_s = PathCompatibilityVariables(model, diagram, z; probability_cut = probability_cut, probability_scale_factor = probability_scale_factor) + end @info "Testing probability_cut" lazy_probability_cut(model, diagram, x_s) @info "Testing expected_value" - if probability_scale_factor > 0 - EV = expected_value(model, diagram, x_s; probability_scale_factor = probability_scale_factor) - else - @test_throws DomainError expected_value(model, diagram, x_s; probability_scale_factor = probability_scale_factor) - end + EV = expected_value(model, diagram, x_s) @info "Testing conditional_value_at_risk" if probability_scale_factor > 0 From 2d2933e7e79d507a34a3b08e6d74ca7f9672afea Mon Sep 17 00:00:00 2001 From: solliolli Date: Fri, 8 Oct 2021 16:39:36 +0300 Subject: [PATCH 121/133] Documentation fixes --- src/analysis.jl | 20 ++++++++++---------- src/decision_model.jl | 4 ++++ src/influence_diagram.jl | 2 +- src/printing.jl | 14 +++++++------- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/analysis.jl b/src/analysis.jl index b0540d58..21bb6754 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -32,7 +32,7 @@ CompatiblePaths outer construction function. Interface for iterating over paths # Examples ```julia -julia> for s in CompatiblePaths(diagram, Z) +for s in CompatiblePaths(diagram, Z) ... end ``` @@ -100,7 +100,7 @@ Construct the probability mass function for path utilities on paths that are com # Examples ```julia -julia> UtilityDistribution(diagram, Z) +UtilityDistribution(diagram, Z) ``` """ function UtilityDistribution(diagram::InfluenceDiagram, Z::DecisionStrategy) @@ -162,8 +162,8 @@ Associate each node with array of conditional probabilities for each of its stat # Examples ```julia # Prior probabilities -julia> prior_probabilities = StateProbabilities(diagram, Z) -julia> StateProbabilities(diagram, Z, Node(2), State(1), prior_probabilities) +prior_probabilities = StateProbabilities(diagram, Z) +StateProbabilities(diagram, Z, Node(2), State(1), prior_probabilities) ``` """ function StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy, node::Node, state::State, prior_probabilities::StateProbabilities) @@ -187,12 +187,12 @@ Associate each node with array of conditional probabilities for each of its stat # Examples ```julia # Prior probabilities -julia> prior_probabilities = StateProbabilities(diagram, Z) +prior_probabilities = StateProbabilities(diagram, Z) # Select node and fix its state -julia> node = "R" -julia> state = "no test" -julia> StateProbabilities(diagram, Z, node, state, prior_probabilities) +node = "R" +state = "no test" +StateProbabilities(diagram, Z, node, state, prior_probabilities) ``` """ function StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy, node::Name, state::Name, prior_probabilities::StateProbabilities) @@ -212,7 +212,7 @@ Associate each node with array of probabilities for each of its states occuring # Examples ```julia -julia> StateProbabilities(diagram, Z) +StateProbabilities(diagram, Z) ``` """ function StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy) @@ -237,7 +237,7 @@ function value_at_risk(U_distribution::UtilityDistribution, α::Float64) end """ - conditional_value_at_risk(u::Vector{Float64}, p::Vector{Float64}, α::Float64) + conditional_value_at_risk(U_distribution::UtilityDistribution, α::Float64) Calculate conditional value-at-risk. """ diff --git a/src/decision_model.jl b/src/decision_model.jl index c9270edc..33774e2e 100644 --- a/src/decision_model.jl +++ b/src/decision_model.jl @@ -133,6 +133,10 @@ function PathCompatibilityVariables(model::Model, probability_cut::Bool=true, probability_scale_factor::Float64=1.0) + if probability_scale_factor ≤ 0 + throw(DomainError("The probability_scale_factor must be greater than 0.")) + end + if !isempty(forbidden_paths) @warn("Forbidden paths is still an experimental feature.") end diff --git a/src/influence_diagram.jl b/src/influence_diagram.jl index 5e59913d..c0d69bff 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -956,7 +956,7 @@ end # --- Generating Diagram --- """ -function generate_diagram!(diagram::InfluenceDiagram; + function generate_diagram!(diagram::InfluenceDiagram; default_probability::Bool=true, default_utility::Bool=true, positive_path_utility::Bool=false, diff --git a/src/printing.jl b/src/printing.jl index aaabd30f..998fcf1c 100644 --- a/src/printing.jl +++ b/src/printing.jl @@ -14,7 +14,7 @@ Print decision strategy. # Examples ```julia ->julia print_decision_strategy(diagram, Z, S_probabilities) +print_decision_strategy(diagram, Z, S_probabilities) ``` """ function print_decision_strategy(diagram::InfluenceDiagram, Z::DecisionStrategy, state_probabilities::StateProbabilities; show_incompatible_states::Bool = false) @@ -47,8 +47,8 @@ Print utility distribution. # Examples ```julia ->julia U_distribution = UtilityDistribution(diagram, Z) ->julia print_utility_distribution(U_distribution) +U_distribution = UtilityDistribution(diagram, Z) +print_utility_distribution(U_distribution) ``` """ function print_utility_distribution(U_distribution::UtilityDistribution; util_fmt="%f", prob_fmt="%f") @@ -60,15 +60,15 @@ function print_utility_distribution(U_distribution::UtilityDistribution; util_fm end """ - print_state_probabilities(sprobs::StateProbabilities, nodes::Vector{Node}; prob_fmt="%f") + print_state_probabilities(diagram::InfluenceDiagram, state_probabilities::StateProbabilities, nodes::Vector{Name}; prob_fmt="%f") Print state probabilities with fixed states. # Examples ```julia ->julia S_probabilities = StateProbabilities(diagram, Z) ->julia print_state_probabilities(S_probabilities, ["R"]) ->julia print_state_probabilities(S_probabilities, ["A"]) +S_probabilities = StateProbabilities(diagram, Z) +print_state_probabilities(S_probabilities, ["R"]) +print_state_probabilities(S_probabilities, ["A"]) ``` """ function print_state_probabilities(diagram::InfluenceDiagram, state_probabilities::StateProbabilities, nodes::Vector{Name}; prob_fmt="%f") From ab82e5d0dd8e4f3c0ad9edb22bda074bda4ba675 Mon Sep 17 00:00:00 2001 From: solliolli Date: Fri, 8 Oct 2021 16:52:03 +0300 Subject: [PATCH 122/133] Fixed tests --- test/decision_model.jl | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/test/decision_model.jl b/test/decision_model.jl index d87f5ec8..b63cd7e2 100644 --- a/test/decision_model.jl +++ b/test/decision_model.jl @@ -31,20 +31,19 @@ function test_decision_model(diagram, n_inactive, probability_scale_factor, prob @info "Testing PathCompatibilityVariables" if probability_scale_factor > 0 x_s = PathCompatibilityVariables(model, diagram, z; probability_cut = probability_cut, probability_scale_factor = probability_scale_factor) - else - @test_throws DomainError x_s = PathCompatibilityVariables(model, diagram, z; probability_cut = probability_cut, probability_scale_factor = probability_scale_factor) - end - @info "Testing probability_cut" - lazy_probability_cut(model, diagram, x_s) + @info "Testing probability_cut" + lazy_probability_cut(model, diagram, x_s) - @info "Testing expected_value" - EV = expected_value(model, diagram, x_s) + @info "Testing expected_value" + EV = expected_value(model, diagram, x_s) - @info "Testing conditional_value_at_risk" - if probability_scale_factor > 0 + @info "Testing conditional_value_at_risk" CVaR = conditional_value_at_risk(model, diagram, x_s, 0.2; probability_scale_factor = probability_scale_factor) else + @test_throws DomainError x_s = PathCompatibilityVariables(model, diagram, z; probability_cut = probability_cut, probability_scale_factor = probability_scale_factor) + + @info "Testing conditional_value_at_risk" @test_throws DomainError conditional_value_at_risk(model, diagram, x_s, 0.2; probability_scale_factor = probability_scale_factor) end From f35907b1d6cec10eaf90b7083159907f9e3e1eab Mon Sep 17 00:00:00 2001 From: solliolli Date: Fri, 8 Oct 2021 17:00:57 +0300 Subject: [PATCH 123/133] Fixing tests --- test/decision_model.jl | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/test/decision_model.jl b/test/decision_model.jl index b63cd7e2..2aeb82ef 100644 --- a/test/decision_model.jl +++ b/test/decision_model.jl @@ -31,19 +31,22 @@ function test_decision_model(diagram, n_inactive, probability_scale_factor, prob @info "Testing PathCompatibilityVariables" if probability_scale_factor > 0 x_s = PathCompatibilityVariables(model, diagram, z; probability_cut = probability_cut, probability_scale_factor = probability_scale_factor) + else + @test_throws DomainError x_s = PathCompatibilityVariables(model, diagram, z; probability_cut = probability_cut, probability_scale_factor = probability_scale_factor) + end + + x_s = PathCompatibilityVariables(model, diagram, z; probability_cut = probability_cut, probability_scale_factor = 1.0) - @info "Testing probability_cut" - lazy_probability_cut(model, diagram, x_s) + @info "Testing probability_cut" + lazy_probability_cut(model, diagram, x_s) - @info "Testing expected_value" - EV = expected_value(model, diagram, x_s) + @info "Testing expected_value" + EV = expected_value(model, diagram, x_s) - @info "Testing conditional_value_at_risk" + @info "Testing conditional_value_at_risk" + if probability_scale_factor > 0 CVaR = conditional_value_at_risk(model, diagram, x_s, 0.2; probability_scale_factor = probability_scale_factor) else - @test_throws DomainError x_s = PathCompatibilityVariables(model, diagram, z; probability_cut = probability_cut, probability_scale_factor = probability_scale_factor) - - @info "Testing conditional_value_at_risk" @test_throws DomainError conditional_value_at_risk(model, diagram, x_s, 0.2; probability_scale_factor = probability_scale_factor) end From e1eb3a8c81bd720e3f4a662318d5d83575afdff5 Mon Sep 17 00:00:00 2001 From: solliolli Date: Mon, 11 Oct 2021 14:13:45 +0300 Subject: [PATCH 124/133] Small improvements to docs --- docs/src/decision-programming/influence-diagram.md | 6 ++++-- docs/src/index.md | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/src/decision-programming/influence-diagram.md b/docs/src/decision-programming/influence-diagram.md index 146c13c4..9fbecc4a 100644 --- a/docs/src/decision-programming/influence-diagram.md +++ b/docs/src/decision-programming/influence-diagram.md @@ -6,15 +6,17 @@ Decision programming uses influence diagrams, a generalization of Bayesian netwo ## Definition ![](figures/linear-graph.svg) -We define the **influence diagram** as a directed, acyclic graph $G=(C,D,V,I,S).$ We describe the nodes $N=C∪D∪V$ with $C∪D=\{1,...,n\}$ and $n=|C|+|D|$ as follows: +We define the **influence diagram** as a directed, acyclic graph $G=(C,D,V,A,S).$ We describe the nodes $N=C∪D∪V$ with $C∪D=\{1,...,n\}$ and $n=|C|+|D|$ as follows: 1) **Chance nodes** $C⊆\{1,...,n\}$ (circles) represent uncertain events associated with random variables. 2) **Decision nodes** $D⊆\{1,...,n\}$ (squares) correspond to decisions among discrete alternatives. 3) **Value nodes** $V=\{n+1,...,n+|V|\}$ (diamonds) represent consequences that result from the realizations of random variables at chance nodes and the decisions made at decision nodes. +The connections between different nodes (arrows) are called **arcs** $a \in A$. The arcs represent different dependencies between the nodes. + We define the **information set** $I$ of node $j∈N$ as -$$I(j)⊆\{i∈C∪D∣i Date: Mon, 11 Oct 2021 17:34:55 +0300 Subject: [PATCH 125/133] Readability improvements --- .../analyzing-decision-strategies.md | 6 ++-- .../computational-complexity.md | 12 +++---- .../decision-programming/decision-model.md | 31 +++++++------------ .../decision-programming/influence-diagram.md | 8 ++--- docs/src/decision-programming/paths.md | 4 +-- 5 files changed, 26 insertions(+), 35 deletions(-) diff --git a/docs/src/decision-programming/analyzing-decision-strategies.md b/docs/src/decision-programming/analyzing-decision-strategies.md index c419e881..50ab88f0 100644 --- a/docs/src/decision-programming/analyzing-decision-strategies.md +++ b/docs/src/decision-programming/analyzing-decision-strategies.md @@ -1,6 +1,6 @@ # [Analyzing Decision Strategies](@id analyzing-decision-strategies) ## Introduction -This section focuses on how we can analyze fixed decision strategies $Z$ on an influence diagram $G$, such as ones resulting from the optimization. We can rule out all incompatible and inactive paths from the analysis because they do not influence the outcomes of the strategy. This means that we only consider paths $𝐬$ that are compatible and active $𝐬 \in 𝐒(X) \cap 𝐒(Z)$. +This section focuses on how we can analyze fixed decision strategies $Z$ on an influence diagram $G$, such as ones obtained by solving the Decision Programming model described in [the previous section](@ref decision-model). We can rule out all incompatible and inactive paths from the analysis because they do not influence the outcomes of the strategy. This means that we only consider paths $𝐬$ that are compatible and active $𝐬 \in 𝐒(X) \cap 𝐒(Z)$. ## Generating Compatible Paths @@ -20,7 +20,7 @@ The probability mass function of the **utility distribution** associates each un $$ℙ(X=u)=∑_{𝐬∈𝐒(Z)∣\mathcal{U}(𝐬)=u} p(𝐬),\quad ∀u∈\mathcal{U}^∗.$$ -From the utility distribution, we can calculate the cumulative distribution, statistics, and risk measures. The relevant statistics are expected value, standard deviation, skewness and kurtosis. Risk measures focus on the conditional value-at-risk (CVaR), also known as, expected shortfall. +From the utility distribution, we can calculate the cumulative distribution, statistics, and risk measures. The relevant statistics are expected value, standard deviation, skewness and kurtosis. Risk measures focus on the conditional value-at-risk (CVaR), also known as expected shortfall. ## Measuring Risk @@ -56,7 +56,7 @@ The above figure demonstrates these values on a discrete probability distributio ## State Probabilities -We denote **paths with fixed states** where $ϵ$ denotes an empty state using a recursive definition. +We use a recursive definition where $ϵ$ denotes an empty state to denote **paths with fixed states**. $$\begin{aligned} 𝐒_{ϵ} &= 𝐒(Z) \\ diff --git a/docs/src/decision-programming/computational-complexity.md b/docs/src/decision-programming/computational-complexity.md index 558650b5..434c5114 100644 --- a/docs/src/decision-programming/computational-complexity.md +++ b/docs/src/decision-programming/computational-complexity.md @@ -32,15 +32,15 @@ $$0 ≤ ∑_{i∈D}|𝐒_{I(i)∪\{i\}}| ≤ |D| \left(\max_{i∈C∪D} |S_j|\ri In the worst case, $m=n$, a decision node is influenced by every other chance and decision node. However, in most practical cases, we have $m < n,$ where decision nodes are influenced only by a limited number of other chance and decision nodes, making models easier to solve. -## Numerical challenges +## Numerical challenges -As has become evident above, in Decision Programming the size of the [Decision Model](@ref decision-model) may become large if the influence diagram has a large number of nodes or nodes with a large number of states. In practice, this results in having a large number of path compatibility and decision variables. This may results in numerical challenges. +As has become evident above, in Decision Programming the size of the [Decision Model](@ref decision-model) may become large if the influence diagram has a large number of nodes or nodes with a large number of states. In practice, this results in having a large number of path compatibility and decision variables. This may result in numerical challenges. ### Probability Scaling Factor -In an influence diagram a large number of nodes or some nodes having a large set of states, causes the path probabilities $p(𝐬)$ to become increasingly small. This may cause numerical issues with the solver or inable it from finding a solution. This issue is showcased in the [CHD preventative care example](../examples/CHD_preventative_care.md). +If an influence diagram has a large number of nodes or some nodes have a large set of states, the path probabilities $p(𝐬)$ become increasingly small. This may cause numerical issues with the solver, even prevent it from finding a solution. This issue is showcased in the [CHD preventative care example](../examples/CHD_preventative_care.md). -The issue may be helped by multiplying the path probabilities with a scaling factor $\gamma > 0$ in the objective function. +The issue may be helped by multiplying the path probabilities with a scaling factor $\gamma > 0$. For example, the objective function becomes -$$\operatorname{E}(Z) = ∑_{𝐬∈𝐒} x(𝐬) \ p(𝐬) \ \gamma \ \mathcal{U}(𝐬)$$ +$$\operatorname{E}(Z) = ∑_{𝐬∈𝐒} x(𝐬) \ p(𝐬) \ \gamma \ \mathcal{U}(𝐬).$$ -The conditional value-at-risk function can also be scaled so that it is compatible with an expected value objective function that has been scaled. \ No newline at end of file +The path probabilities should also be scaled in other objective functions or constraints, including the conditional value-at-risk function and the probability cut constraint $∑_{𝐬∈𝐒}x(𝐬) p(𝐬) = 1$. diff --git a/docs/src/decision-programming/decision-model.md b/docs/src/decision-programming/decision-model.md index d55068eb..fba670b5 100644 --- a/docs/src/decision-programming/decision-model.md +++ b/docs/src/decision-programming/decision-model.md @@ -1,14 +1,14 @@ # [Decision Model](@id decision-model) ## Introduction -**Decision programming** aims to find an optimal decision strategy $Z$ from all decision strategies $ℤ$ by maximizing an objective function $f$ on the path distribution of an influence diagram +**Decision Programming** aims to find an optimal decision strategy $Z$ among all decision strategies $ℤ$ by maximizing an objective function $f$ on the path distribution of an influence diagram $$\underset{Z∈ℤ}{\text{maximize}}\quad f(\{(ℙ(X=𝐬∣Z), \mathcal{U}(𝐬)) ∣ 𝐬∈𝐒\}). \tag{1}$$ -**Decision model** refers to the mixed-integer linear programming formulation of this optimization problem. This page explains how to express decision strategies, compatible paths, path utilities and the objective of the model as a mixed-integer linear program. We present two standard objective functions, including expected value and risk measures. The original decision model formulation was described in [^1], sections 3 and 5. We base the decision model on an improved formulation described in [^2] section 3.3. We recommend reading the references for motivation, details, and proofs of the formulation. +**Decision model** refers to the mixed-integer linear programming formulation of this optimization problem. This page explains how to express decision strategies, compatible paths, path utilities and the objective of the model as a mixed-integer linear program. We present two standard objective functions, including expected value and conditional value-at-risk. The original decision model formulation was described in [^1], sections 3 and 5. We base the decision model on an improved formulation described in [^2] section 3.3. We recommend reading the references for motivation, details, and proofs of the formulation. ## Decision Variables -**Decision variables** $z(s_j∣𝐬_{I(j)})$ are equivalent to local decision strategies such that $Z_j(𝐬_{I(j)})=s_j$ if and only if $z(s_j∣𝐬_{I(j)})=1$ and $z(s_{j}^′∣𝐬_{I(j)})=0$ for all $s_{j}^′∈S_j∖s_j.$ Constraint $(2)$ defines the decisions to be binary variables and the constraint $(3)$ limits decisions to one per information path. +**Decision variables** $z(s_j∣𝐬_{I(j)})$ are equivalent to local decision strategies such that $Z_j(𝐬_{I(j)})=s_j$ if and only if $z(s_j∣𝐬_{I(j)})=1$ and $z(s_{j}^′∣𝐬_{I(j)})=0$ for all $s_{j}^′∈S_j∖s_j.$ Constraint $(2)$ defines the decisions to be binary variables and the constraint $(3)$ states that only one decision alternative $s_{j}$ can be chosen for each information set $s_{I(j)}$. $$z(s_j∣𝐬_{I(j)}) ∈ \{0,1\},\quad ∀j∈D, s_j∈S_j, 𝐬_{I(j)}∈𝐒_{I(j)} \tag{2}$$ @@ -16,7 +16,7 @@ $$∑_{s_j∈S_j} z(s_j∣𝐬_{I(j)})=1,\quad ∀j∈D, 𝐬_{I(j)}∈𝐒_{I(j ## Path Compatibility Variables -**Path compatibility variables** $x(𝐬)$ are indicator variables for whether path $𝐬$ is compatible with decision strategy $Z$ that is defined by the decision variables $z$. These are continous variables but only assume binary values, so that the compatible paths $𝐬 ∈ 𝐒(Z)$ take values $x(𝐬) = 1$ and other paths $𝐬 ∈ 𝐒 \setminus 𝐒(Z)$ take values $x(𝐬) = 0$. Constraint $(4)$ defines the lower and upper bounds for the variables. +**Path compatibility variables** $x(𝐬)$ are indicator variables for whether path $𝐬$ is compatible with decision strategy $Z$ defined by the decision variables $z$. These are continous variables but only assume binary values, so that the compatible paths $𝐬 ∈ 𝐒(Z)$ take values $x(𝐬) = 1$ and other paths $𝐬 ∈ 𝐒 \setminus 𝐒(Z)$ take values $x(𝐬) = 0$. Constraint $(4)$ defines the lower and upper bounds for the variables. $$0≤x(𝐬)≤1,\quad ∀𝐬∈𝐒 \tag{4}$$ @@ -53,7 +53,7 @@ The motivation for using the minimum of these bounds is that it depends on the p ## Lazy Probability Cut -Constraint $(6)$ is a complicating constraint and thus adding it directly to the model may slow down the overall solution process. It may be beneficial to instead add it as a *lazy constraint*. In the solver, a lazy constraint is only generated when an incumbent solution violates it. In some instances, this allows the MILP solver to prune nodes of the branch-and-bound tree more efficiently. +Constraint $(6)$ is a complicating constraint involving all path compatibility variables $x(s)$ and thus adding it directly to the model may slow down the overall solution process. It may be beneficial to instead add it as a *lazy constraint*. In the solver, a lazy constraint is only generated when an incumbent solution violates it. In some instances, this allows the MILP solver to prune nodes of the branch-and-bound tree more efficiently. ## Expected Value @@ -61,17 +61,13 @@ The **expected value** objective is defined using the path compatibility variabl $$\operatorname{E}(Z) = ∑_{𝐬∈𝐒} x(𝐬) \ p(𝐬) \ \mathcal{U}(𝐬). \tag{7}$$ -## Positive Path Utility -We can omit the probability cut defined in constraint $(6)$ from the model if we are maximising expected value of utility and use a **positive path utility** function $\mathcal{U}^+$. The positive path utility function $\mathcal{U}^+$ is an affine transformation of path utility function $\mathcal{U}$ which translates all utility values to positive values. As an example, we can subtract the minimum of the original utility function and then add one as follows. +## Positive and Negative Path Utilities +We can omit the probability cut defined in constraint $(6)$ from the model if we are maximising expected value of utility and use a **positive path utility** function $\mathcal{U}^+$. Similarly, we can use a **negative path utility** function $\mathcal{U}^-$ when minimizing expected value. These functions are affine transformations of the path utility function $\mathcal{U}$ which translate all utility values to positive/negative values. As an example of a positive path utility function, we can subtract the minimum of the original utility function and then add one as follows. $$\mathcal{U}^+(𝐬) = \mathcal{U}(𝐬) - \min_{𝐬∈𝐒} \mathcal{U}(𝐬) + 1. \tag{8}$$ -## Negative Path Utility -We can omit the probability cut defined in constraint $(6)$ from the model if we are minimising expected value of utility and use a **negative path utility** function $\mathcal{U}^-$. This affine transformation of the path utility function $\mathcal{U}$ translates all utility values to negative values. As an example, we can subtract the maximum of the original utility function and then subtract one as follows. - $$\mathcal{U}^-(𝐬) = \mathcal{U}(𝐬) - \max_{𝐬∈𝐒} \mathcal{U}(𝐬) - 1. \tag{9}$$ - ## Conditional Value-at-Risk The section [Measuring Risk](@ref) explains and visualizes the relationships between the formulation of expected value, value-at-risk and conditional value-at-risk for discrete probability distribution. @@ -91,7 +87,7 @@ $$𝐒_{α}^{=}=\{𝐬∈𝐒∣\mathcal{U}(𝐬)=u_α\}.$$ We define **conditional value-at-risk** as -$$\operatorname{CVaR}_α(Z)=\frac{1}{α}\left(∑_{𝐬∈𝐒_α^{<}} x(𝐬) \ p(𝐬) \ \mathcal{U}(𝐬) + ∑_{𝐬∈𝐒_α^{=}} \left(α - ∑_{𝐬∈𝐒_α^{<}} x(𝐬) \ p(𝐬) \right) \mathcal{U}(𝐬) \right).$$ +$$\operatorname{CVaR}_α(Z)=\frac{1}{α}\left(∑_{𝐬∈𝐒_α^{<}} x(𝐬) \ p(𝐬) \ \mathcal{U}(𝐬) + ∑_{𝐬∈𝐒_α^{=}} \left(α - ∑_{𝐬'∈𝐒_α^{<}} x(𝐬') \ p(𝐬') \right) \mathcal{U}(𝐬) \right).$$ We can form the conditional value-at-risk as an optimization problem. We have the following pre-computed parameters. @@ -101,13 +97,13 @@ $$\operatorname{VaR}_0(Z)=u^-=\min\{\mathcal{U}(𝐬)∣𝐬∈𝐒\}, \tag{11}$ $$\operatorname{VaR}_1(Z)=u^+=\max\{\mathcal{U}(𝐬)∣𝐬∈𝐒\}. \tag{12}$$ -Largest difference between path utilities +A "large number", specifically the largest difference between path utilities $$M=u^+-u^-. \tag{13}$$ -Half of the smallest positive difference between path utilities +A "small number", specifically half of the smallest positive difference between path utilities -$$ϵ=\frac{1}{2} \min\{|\mathcal{U}(𝐬)-\mathcal{U}(𝐬^′)| ∣ |\mathcal{U}(𝐬)-\mathcal{U}(𝐬^′)| > 0, 𝐬, 𝐬^′∈𝐒\}. \tag{14}$$ +$$ϵ=\frac{1}{2} \min\{|\mathcal{U}(𝐬)-\mathcal{U}(𝐬^′)| \mid |\mathcal{U}(𝐬)-\mathcal{U}(𝐬^′)| > 0, 𝐬, 𝐬^′∈𝐒\}. \tag{14}$$ The objective is to minimize the variable $η$ whose optimal value is equal to the value-at-risk, that is, $\operatorname{VaR}_α(Z)=\min η.$ @@ -139,11 +135,6 @@ We can express the conditional value-at-risk objective as $$\operatorname{CVaR}_α(Z)=\frac{1}{α}∑_{𝐬∈𝐒}\bar{ρ}(𝐬) \mathcal{U}(𝐬)\tag{25}.$$ -The values of conditional value-at-risk are limited to the interval between the lower bound of value-at-risk and the expected value - -$$\operatorname{VaR}_0(Z)<\operatorname{CVaR}_α(Z)≤E(Z).$$ - - ## Convex Combination We can combine expected value and conditional value-at-risk using a convex combination at a fixed probability level $α∈(0, 1]$ as follows diff --git a/docs/src/decision-programming/influence-diagram.md b/docs/src/decision-programming/influence-diagram.md index 9fbecc4a..8353898a 100644 --- a/docs/src/decision-programming/influence-diagram.md +++ b/docs/src/decision-programming/influence-diagram.md @@ -105,13 +105,13 @@ Otherwise, it is **inactive**. ## Decision Strategies -Each decision strategy models how the decision maker chooses a state $s_j∈S_j$ given an information state $𝐬_{I(j)}$ at decision node $j∈D.$ A decision node is a special type of chance node, such that the probability of the chosen state given an information state is fixed to one +Each decision strategy models how the decision maker chooses a state $s_j∈S_j$ given an information state $𝐬_{I(j)}$ at decision node $j∈D.$ A decision node can be seen as a special type of chance node, such that the probability of the chosen state given an information state is fixed to one $$ℙ(X_j=s_j∣X_{I(j)}=𝐬_{I(j)})=1.$$ By definition, the probabilities for other states are zero. -Formally, for each decision node $j∈D,$ a **local decision strategy** is function that maps an information state $𝐬_{I(j)}$ to a state $s_j$ +Formally, for each decision node $j∈D,$ a **local decision strategy** is a function that maps an information state $𝐬_{I(j)}$ to a state $s_j$ $$Z_j:𝐒_{I(j)}↦S_j.$$ @@ -142,7 +142,7 @@ $$q(𝐬∣Z) = ∏_{j∈D} ℙ(X_j=𝐬_j∣X_{I(j)}=𝐬_{I(j)}).$$ Because the probabilities of decision nodes are defined as one or zero depending on the decision strategy, we can simplify the second part to an indicator function $$q(𝐬∣Z)=\begin{cases} -1, & x(𝐬) \\ +1, & x(𝐬) = 1 \\ 0, & \text{otherwise} \end{cases}.$$ @@ -207,7 +207,7 @@ Two nodes are **sequential** if there exists a directed path from one node to th **Repeated subdiagram** refers to a recurring pattern within an influence diagram. Often, influence diagrams do not have a unique structure, but they consist of a repeated pattern due to the underlying problem's properties. -**Limited-memory** influence diagram refers to an influence diagram where an upper bound limits the size of the information set for decision nodes. That is, $I(j)≤m$ for all $j∈D$ where the limit $m$ is less than $|C∪D|.$ Smaller limits of $m$ are desirable because they reduce the decision model size, as discussed on the [Computational Complexity](@ref computational-complexity) page. +**Limited-memory** influence diagram refers to an influence diagram where an upper bound limits the size of the information set for decision nodes. That is, $\mid I(j) \mid ≤m$ for all $j∈D$ where the limit $m$ is less than $|C∪D|.$ Smaller limits of $m$ are desirable because they reduce the decision model size, as discussed on the [Computational Complexity](@ref computational-complexity) page. **Isolated subdiagrams** refer to unconnected diagrams within an influence diagram. That is, there are no undirected connections between the diagrams. Therefore, one isolated subdiagram's decisions affect decisions on the other isolated subdiagrams only through the utility function. diff --git a/docs/src/decision-programming/paths.md b/docs/src/decision-programming/paths.md index b2b7f742..2f85953a 100644 --- a/docs/src/decision-programming/paths.md +++ b/docs/src/decision-programming/paths.md @@ -8,9 +8,9 @@ Formally, the path $𝐬$ is **ineffective** if and only if $𝐬_A∈𝐒_A^′ $$𝐒^∗=\{𝐬∈𝐒∣𝐬_{A}∉𝐒_{A}^′\}⊆𝐒.$$ -The [Decision Model](@ref decision-model) size depends on the number of effective paths, rather than the number of paths or size of the influence diagram directly. If effective paths is empty, the influence diagram has no solutions. +The [Decision Model](@ref decision-model) size depends on the number of effective paths, rather than the number of paths or size of the influence diagram directly. -In Decision Programming, one can declare certain subpaths to be effective or ineffective using the *fixed path* and *forbidden paths* sets. +In Decision Programming, one can declare certain subpaths to be ineffective using the *fixed path* and *forbidden paths* sets. ### Fixed Path **Fixed path** refers to a subpath which must be realized. If the fixed path is $s_Y = S_Y^f$ for all nodes $Y⊆C∪D$, then the effective paths in the model are From cf53a0ce65f5e321c2f3d07aa45e071f68457c6a Mon Sep 17 00:00:00 2001 From: Olli Herrala <43684983+solliolli@users.noreply.github.com> Date: Tue, 12 Oct 2021 10:12:24 +0300 Subject: [PATCH 126/133] Apply suggestions from code review Co-authored-by: paulaweller <72554386+paulaweller@users.noreply.github.com> --- docs/src/decision-programming/influence-diagram.md | 4 ++-- docs/src/usage.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/decision-programming/influence-diagram.md b/docs/src/decision-programming/influence-diagram.md index 8353898a..92f222a2 100644 --- a/docs/src/decision-programming/influence-diagram.md +++ b/docs/src/decision-programming/influence-diagram.md @@ -14,11 +14,11 @@ We define the **influence diagram** as a directed, acyclic graph $G=(C,D,V,A,S). The connections between different nodes (arrows) are called **arcs** $a \in A$. The arcs represent different dependencies between the nodes. -We define the **information set** $I$ of node $j∈N$ as +We define the **information set** $I$ of node $j∈N$ as the set of predecessors of $j$ in the graph: $$I(j)⊆\{i∈C∪D ∣ (i,j) \in A\, i diagram.X ``` -As another example, we will add the probability matrix of node C2. It has two nodes in its information set: C1 and D1. These nodes have 3 and 2 state respectively. Node C2 itself has 2 states. The question is should the dimensions of the probability matrix be $(|S_{C1}|, |\ S_{D1}|, |\ S_{C2}|) = (3, 2, 2)$ or $(|S_{D1}|, |\ S_{C1}|, \ |S_{C2}|) = (2, 3, 2)$? The answer is that the dimensions should be in ascending order of the nodes' numbers that they correspond to. This is also the order that the information set is in in the field `I_j`. In this case the influence diagram looks like this: +As another example, we will add the probability matrix of node C2. It has two nodes in its information set: C1 and D1. These nodes have 3 and 2 states, respectively. Node C2 itself has 2 states. Now, the question is: should the dimensions of the probability matrix be $(|S_{C1}|, |\ S_{D1}|, |\ S_{C2}|) = (3, 2, 2)$ or $(|S_{D1}|, |\ S_{C1}|, \ |S_{C2}|) = (2, 3, 2)$? The answer is that the dimensions should be in ascending order of the nodes' numbers that they correspond to. This is also the order that the information set is in in the field `I_j`. In this case the influence diagram looks like this: ```julia julia> diagram.Names 4-element Array{String,1}: From 03d2908f35cb3af1b9918cea6740ca2b72e53e4c Mon Sep 17 00:00:00 2001 From: solliolli Date: Tue, 12 Oct 2021 11:03:56 +0300 Subject: [PATCH 127/133] More readability improvements --- .../decision-programming/influence-diagram.md | 16 ++++++++-------- docs/src/decision-programming/paths.md | 6 +++--- docs/src/examples/pig-breeding.md | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/src/decision-programming/influence-diagram.md b/docs/src/decision-programming/influence-diagram.md index 92f222a2..dbc3053c 100644 --- a/docs/src/decision-programming/influence-diagram.md +++ b/docs/src/decision-programming/influence-diagram.md @@ -16,7 +16,7 @@ The connections between different nodes (arrows) are called **arcs** $a \in A$. We define the **information set** $I$ of node $j∈N$ as the set of predecessors of $j$ in the graph: -$$I(j)⊆\{i∈C∪D ∣ (i,j) \in A\, i Date: Wed, 13 Oct 2021 11:33:50 +0300 Subject: [PATCH 128/133] Prettier printing for state probabilities --- docs/src/examples/n-monitoring.md | 68 +++++++++++++++---------------- docs/src/examples/pig-breeding.md | 6 +-- src/printing.jl | 28 ++++++++----- 3 files changed, 54 insertions(+), 48 deletions(-) diff --git a/docs/src/examples/n-monitoring.md b/docs/src/examples/n-monitoring.md index 25118df3..eb8c2bff 100644 --- a/docs/src/examples/n-monitoring.md +++ b/docs/src/examples/n-monitoring.md @@ -244,40 +244,40 @@ julia> print_decision_strategy(diagram, Z, S_probabilities) The state probabilities for strategy $Z$ are also obtained. These tell the probability of each state in each node, given strategy $Z$. ```julia-repl -julia> print_state_probabilities(sprobs, L) -┌───────┬──────────┬──────────┬─────────────┐ -│ Node │ State 1 │ State 2 │ Fixed state │ -│ Int64 │ Float64 │ Float64 │ String │ -├───────┼──────────┼──────────┼─────────────┤ -│ 1 │ 0.564449 │ 0.435551 │ │ -└───────┴──────────┴──────────┴─────────────┘ -julia> print_state_probabilities(sprobs, R_k) -┌───────┬──────────┬──────────┬─────────────┐ -│ Node │ State 1 │ State 2 │ Fixed state │ -│ Int64 │ Float64 │ Float64 │ String │ -├───────┼──────────┼──────────┼─────────────┤ -│ 2 │ 0.515575 │ 0.484425 │ │ -│ 3 │ 0.442444 │ 0.557556 │ │ -│ 4 │ 0.543724 │ 0.456276 │ │ -│ 5 │ 0.552515 │ 0.447485 │ │ -└───────┴──────────┴──────────┴─────────────┘ -julia> print_state_probabilities(sprobs, A_k) -┌───────┬──────────┬──────────┬─────────────┐ -│ Node │ State 1 │ State 2 │ Fixed state │ -│ Int64 │ Float64 │ Float64 │ String │ -├───────┼──────────┼──────────┼─────────────┤ -│ 6 │ 1.000000 │ 0.000000 │ │ -│ 7 │ 1.000000 │ 0.000000 │ │ -│ 8 │ 1.000000 │ 0.000000 │ │ -│ 9 │ 1.000000 │ 0.000000 │ │ -└───────┴──────────┴──────────┴─────────────┘ -julia> print_state_probabilities(sprobs, F) -┌───────┬──────────┬──────────┬─────────────┐ -│ Node │ State 1 │ State 2 │ Fixed state │ -│ Int64 │ Float64 │ Float64 │ String │ -├───────┼──────────┼──────────┼─────────────┤ -│ 10 │ 0.633125 │ 0.366875 │ │ -└───────┴──────────┴──────────┴─────────────┘ +julia> print_state_probabilities(diagram, S_probabilities, ["L"]) +┌────────┬──────────┬──────────┬─────────────┐ +│ Node │ high │ low │ Fixed state │ +│ String │ Float64 │ Float64 │ String │ +├────────┼──────────┼──────────┼─────────────┤ +│ L │ 0.564449 │ 0.435551 │ │ +└────────┴──────────┴──────────┴─────────────┘ +julia> print_state_probabilities(diagram, S_probabilities, [["R$i" for i in 1:N]...]) +┌────────┬──────────┬──────────┬─────────────┐ +│ Node │ high │ low │ Fixed state │ +│ String │ Float64 │ Float64 │ String │ +├────────┼──────────┼──────────┼─────────────┤ +│ R1 │ 0.515575 │ 0.484425 │ │ +│ R2 │ 0.442444 │ 0.557556 │ │ +│ R3 │ 0.543724 │ 0.456276 │ │ +│ R4 │ 0.552515 │ 0.447485 │ │ +└────────┴──────────┴──────────┴─────────────┘ +julia> print_state_probabilities(diagram, S_probabilities, [["A$i" for i in 1:N]...]) +┌────────┬──────────┬──────────┬─────────────┐ +│ Node │ yes │ no │ Fixed state │ +│ String │ Float64 │ Float64 │ String │ +├────────┼──────────┼──────────┼─────────────┤ +│ A1 │ 1.000000 │ 0.000000 │ │ +│ A2 │ 1.000000 │ 0.000000 │ │ +│ A3 │ 1.000000 │ 0.000000 │ │ +│ A4 │ 1.000000 │ 0.000000 │ │ +└────────┴──────────┴──────────┴─────────────┘ +julia> print_state_probabilities(diagram, S_probabilities, ["F"]) +┌────────┬──────────┬──────────┬─────────────┐ +│ Node │ failure │ success │ Fixed state │ +│ String │ Float64 │ Float64 │ String │ +├────────┼──────────┼──────────┼─────────────┤ +│ F │ 0.633125 │ 0.366875 │ │ +└────────┴──────────┴──────────┴─────────────┘ ``` We can also print the utility distribution for the optimal strategy and some basic statistics for the distribution. diff --git a/docs/src/examples/pig-breeding.md b/docs/src/examples/pig-breeding.md index 7e3a8178..a1b4adce 100644 --- a/docs/src/examples/pig-breeding.md +++ b/docs/src/examples/pig-breeding.md @@ -241,7 +241,7 @@ julia> health_nodes = [["H$i" for i in 1:N]...] julia> print_state_probabilities(diagram, S_probabilities, health_nodes) ┌────────┬──────────┬──────────┬─────────────┐ -│ Node │ State 1 │ State 2 │ Fixed state │ +│ Node │ ill │ healthy │ Fixed state │ │ String │ Float64 │ Float64 │ String │ ├────────┼──────────┼──────────┼─────────────┤ │ H1 │ 0.100000 │ 0.900000 │ │ @@ -253,7 +253,7 @@ julia> print_state_probabilities(diagram, S_probabilities, health_nodes) julia> test_nodes = [["T$i" for i in 1:N-1]...] julia> print_state_probabilities(diagram, S_probabilities, test_nodes) ┌────────┬──────────┬──────────┬─────────────┐ -│ Node │ State 1 │ State 2 │ Fixed state │ +│ Node │ positive │ negative │ Fixed state │ │ String │ Float64 │ Float64 │ String │ ├────────┼──────────┼──────────┼─────────────┤ │ T1 │ 0.170000 │ 0.830000 │ │ @@ -264,7 +264,7 @@ julia> print_state_probabilities(diagram, S_probabilities, test_nodes) julia> treatment_nodes = [["D$i" for i in 1:N-1]...] julia> print_state_probabilities(diagram, S_probabilities, treatment_nodes) ┌────────┬──────────┬──────────┬─────────────┐ -│ Node │ State 1 │ State 2 │ Fixed state │ +│ Node │ treat │ pass │ Fixed state │ │ String │ Float64 │ Float64 │ String │ ├────────┼──────────┼──────────┼─────────────┤ │ D1 │ 0.000000 │ 1.000000 │ │ diff --git a/src/printing.jl b/src/printing.jl index 998fcf1c..7f906aaa 100644 --- a/src/printing.jl +++ b/src/printing.jl @@ -73,23 +73,29 @@ print_state_probabilities(S_probabilities, ["A"]) """ function print_state_probabilities(diagram::InfluenceDiagram, state_probabilities::StateProbabilities, nodes::Vector{Name}; prob_fmt="%f") node_indices = [findfirst(j -> j==node, diagram.Names) for node in nodes] + states_list = diagram.States[node_indices] + state_sets = unique(states_list) + n = length(states_list) probs = state_probabilities.probs fixed = state_probabilities.fixed prob(p, state) = if 1≤state≤length(p) p[state] else NaN end - fix_state(i) = if i∈keys(fixed) string(fixed[i]) else "" end - - # Maximum number of states - limit = maximum(length(probs[i]) for i in node_indices) - states = 1:limit - df = DataFrame() - df[!, :Node] = nodes - for state in states - df[!, Symbol("State $state")] = [prob(probs[i], state) for i in node_indices] + fix_state(i) = if i∈keys(fixed) string(diagram.States[i][fixed[i]]) else "" end + + + for state_set in state_sets + node_indices2 = filter(i -> diagram.States[i] == state_set, node_indices) + state_names = diagram.States[node_indices2[1]] + states = 1:length(state_names) + df = DataFrame() + df[!, :Node] = diagram.Names[node_indices2] + for state in states + df[!, Symbol("$(state_names[state])")] = [prob(probs[i], state) for i in node_indices2] + end + df[!, Symbol("Fixed state")] = [fix_state(i) for i in node_indices2] + pretty_table(df; formatters = ft_printf(prob_fmt, (first(states)+1):(last(states)+1))) end - df[!, Symbol("Fixed state")] = [fix_state(i) for i in node_indices] - pretty_table(df; formatters = ft_printf(prob_fmt, (first(states)+1):(last(states)+1))) end """ From 6b18576dee40c4664dad4df8369c4fcf664da789 Mon Sep 17 00:00:00 2001 From: Fabricio Oliveira Date: Thu, 14 Oct 2021 11:50:22 +0300 Subject: [PATCH 129/133] Update docs/src/examples/used-car-buyer.md Co-authored-by: paulaweller <72554386+paulaweller@users.noreply.github.com> --- docs/src/examples/used-car-buyer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/examples/used-car-buyer.md b/docs/src/examples/used-car-buyer.md index 631a2a95..a2979c3f 100644 --- a/docs/src/examples/used-car-buyer.md +++ b/docs/src/examples/used-car-buyer.md @@ -48,7 +48,7 @@ The second chance node $R$ has nodes $O$ and $T$ in its information set, and thr add_node!(diagram, ChanceNode("R", ["O", "T"], ["no test", "lemon", "peach"])) ``` -### Purchace decision +### Purchase decision The purchase decision represented by node $A$ is added as follows. ```julia add_node!(diagram, DecisionNode("A", ["R"], ["buy without guarantee", "buy with guarantee", "don't buy"])) From 0cb6d49704ad40ff2a19ab47716496c8fb143153 Mon Sep 17 00:00:00 2001 From: Olli Herrala <43684983+solliolli@users.noreply.github.com> Date: Thu, 21 Oct 2021 10:03:48 +0300 Subject: [PATCH 130/133] Apply suggestions from code review Co-authored-by: paulaweller <72554386+paulaweller@users.noreply.github.com> --- docs/src/examples/n-monitoring.md | 6 +++--- docs/src/examples/pig-breeding.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/examples/n-monitoring.md b/docs/src/examples/n-monitoring.md index eb8c2bff..aafc75ae 100644 --- a/docs/src/examples/n-monitoring.md +++ b/docs/src/examples/n-monitoring.md @@ -6,7 +6,7 @@ The $N$-monitoring problem is described in [^1], sections 4.1 and 6.1. ## Influence Diagram ![](figures/n-monitoring.svg) -The influence diagram of the generalized $N$-monitoring problem where $N≥1$ and indices $k=1,...,N.$ The nodes are associated with states as follows. **Load state** $L=\{high, low\}$ denotes the load on a structure, **report states** $R_k=\{high, low\}$ report the load state to the **action states** $A_k=\{yes, no\}$ which represent different decisions to fortify the structure. The **failure state** $F=\{failure, success\}$ represents whether or not the (fortified) structure fails under the load $L$. Finally, the utility at target $T$ depends on the whether $F$ fails and the fortification costs. +The influence diagram of the generalized $N$-monitoring problem where $N≥1$ and indices $k=1,...,N.$ The nodes are associated with states as follows. **Load state** $L=\{high, low\}$ denotes the load on a structure, **report states** $R_k=\{high, low\}$ report the load state to the **action states** $A_k=\{yes, no\}$ which represent different decisions to fortify the structure. The **failure state** $F=\{failure, success\}$ represents whether or not the (fortified) structure fails under the load $L$. Finally, the utility at target $T$ depends on the fortification costs and whether F fails. We begin by choosing $N$ and defining our fortification cost function. We draw the cost of fortification $c_k∼U(0,1)$ from a uniform distribution, and the magnitude of fortification is directly proportional to the cost. Fortification is defined as @@ -115,7 +115,7 @@ X_F = ProbabilityMatrix(diagram, "F") This matrix has dimensions $(2, \textcolor{orange}{2, 2, 2, 2}, 2)$ because node $L$ and nodes $A_k$, which form the information set of $F$, all have 2 states and node $F$ itself also has 2 states. The orange colored dimensions correspond to the states of the action nodes $A_k$. -To set the probabilities we have to iterate over the information states. Here it helps to know that in Decision Programming the states of each node are mapped to numbers in the back-end. For instance, the load states $high$ and $low$ are referred to as 1 and 2. The same applies for the action states $yes$ and $no$, they are states 1 and 2. The `paths` function allows us to iterate over the subpaths of specific nodes. In these paths, the states are refer to by their indices. Using this information, we can easily iterate over the information states using the `paths` function and set the probability values into the probability matrix. +To set the probabilities we have to iterate over the information states. Here it helps to know that in Decision Programming the states of each node are mapped to numbers in the back-end. For instance, the load states $high$ and $low$ are referred to as 1 and 2. The same applies for the action states $yes$ and $no$, they are states 1 and 2. The `paths` function allows us to iterate over the subpaths of specific nodes. In these paths, the states are referred to by their indices. Using this information, we can easily iterate over the information states using the `paths` function and enter the probability values into the probability matrix. ```julia @@ -139,7 +139,7 @@ The utility from the different scenarios of the failure state at target $T$ are $$g(F=failure) = 0$$ -$$g(F=success) = 100$$. +$$g(F=success) = 100.$$ Utilities from the action states $A_k$ at target $T$ are diff --git a/docs/src/examples/pig-breeding.md b/docs/src/examples/pig-breeding.md index a1b4adce..fd21dde8 100644 --- a/docs/src/examples/pig-breeding.md +++ b/docs/src/examples/pig-breeding.md @@ -93,7 +93,7 @@ $$ℙ(h_k = ill ∣ h_{k-1} = ill, \ d_{k-1} = pass)=0.9,$$ $$ℙ(h_k = ill ∣ h_{k-1} = ill, \ d_{k-1} = treat)=0.5.$$ -In Decision Programming, the probability matrix is define in the following way. Notice, that the ordering of the information state corresponds to the order in which the information set was defined when adding the health nodes. +In Decision Programming, the probability matrix is defined in the following way. Notice, that the ordering of the information state corresponds to the order in which the information set was defined when adding the health nodes. ```julia X_H = ProbabilityMatrix(diagram, "H2") X_H["healthy", "pass", :] = [0.2, 0.8] From a6b878aff32441a25555bf2d076118d969cd7b1e Mon Sep 17 00:00:00 2001 From: solliolli Date: Thu, 21 Oct 2021 10:19:37 +0300 Subject: [PATCH 131/133] Small documentation improvements --- docs/src/examples/contingent-portfolio-programming.md | 4 ++++ docs/src/examples/pig-breeding.md | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/src/examples/contingent-portfolio-programming.md b/docs/src/examples/contingent-portfolio-programming.md index 10c9e931..94965606 100644 --- a/docs/src/examples/contingent-portfolio-programming.md +++ b/docs/src/examples/contingent-portfolio-programming.md @@ -1,4 +1,8 @@ # Contingent Portfolio Programming + +!!! warning + This example discusses adding constraints and decision variables to the Decision Programming formulation, as well as custom path utility calculation. Because of this, it is quite advanced compared to the earlier ones. + ## Description [^1], section 4.2 diff --git a/docs/src/examples/pig-breeding.md b/docs/src/examples/pig-breeding.md index fd21dde8..425df520 100644 --- a/docs/src/examples/pig-breeding.md +++ b/docs/src/examples/pig-breeding.md @@ -14,7 +14,7 @@ The pig breeding problem as described in [^1]. The influence diagram for the generalized $N$-month pig breeding problem. The nodes are associated with the following states. **Health states** $h_k=\{ill,healthy\}$ represent the health of the pig at month $k=1,...,N$. **Test states** $t_k=\{positive,negative\}$ represent the result from testing the pig at month $k=1,...,N-1$. **Treatment states** $d_k=\{treat, pass\}$ represent the decision to treat the pig with an injection at month $k=1,...,N-1$. -> The dashed arcs represent the no-forgetting principle and we can toggle them on and off in the formulation. +> The dashed arcs represent the no-forgetting principle. The no-forgetting assumption does not hold without them and they are tnot included in the following model. They could be included by changing the information sets of nodes. In this example, we solve the 4 month pig breeding problem and thus, declare $N = 4$. @@ -70,7 +70,7 @@ generate_arcs!(diagram) ### Probabilities -We define probability distributions for all chance nodes. For the first health node, the probability distribution is defined over its two states $ill$ and $healthy$. The probability that pig is ill in the first month is +We define probability distributions for all chance nodes. For the first health node, the probability distribution is defined over its two states $ill$ and $healthy$. The probability that the pig is ill in the first month is $$ℙ(h_1 = ill)=0.1.$$ From ec941c2bf43178cd9eabcea1b398549166b32cdf Mon Sep 17 00:00:00 2001 From: solliolli Date: Thu, 21 Oct 2021 11:07:27 +0300 Subject: [PATCH 132/133] Small fix to CVaR calculation when all utilities are the same. --- docs/src/decision-programming/decision-model.md | 2 +- src/decision_model.jl | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/src/decision-programming/decision-model.md b/docs/src/decision-programming/decision-model.md index fba670b5..fde8d78a 100644 --- a/docs/src/decision-programming/decision-model.md +++ b/docs/src/decision-programming/decision-model.md @@ -87,7 +87,7 @@ $$𝐒_{α}^{=}=\{𝐬∈𝐒∣\mathcal{U}(𝐬)=u_α\}.$$ We define **conditional value-at-risk** as -$$\operatorname{CVaR}_α(Z)=\frac{1}{α}\left(∑_{𝐬∈𝐒_α^{<}} x(𝐬) \ p(𝐬) \ \mathcal{U}(𝐬) + ∑_{𝐬∈𝐒_α^{=}} \left(α - ∑_{𝐬'∈𝐒_α^{<}} x(𝐬') \ p(𝐬') \right) \mathcal{U}(𝐬) \right).$$ +$$\operatorname{CVaR}_α(Z)=\frac{1}{α}\left(∑_{𝐬∈𝐒_α^{<}} x(𝐬) \ p(𝐬) \ \mathcal{U}(𝐬) + \left(α - ∑_{𝐬'∈𝐒_α^{<}} x(𝐬') \ p(𝐬') \right) u_α \right).$$ We can form the conditional value-at-risk as an optimization problem. We have the following pre-computed parameters. diff --git a/src/decision_model.jl b/src/decision_model.jl index 33774e2e..5ade4415 100644 --- a/src/decision_model.jl +++ b/src/decision_model.jl @@ -263,7 +263,11 @@ function conditional_value_at_risk(model::Model, u_max = u_sorted[end] M = u_max - u_min u_diff = diff(u_sorted) - ϵ = if isempty(u_diff) 0.0 else minimum(filter(!iszero, abs.(u_diff))) / 2 end + if isempty(filter(!iszero, u_diff)) + return u_min # All utilities are the same, CVaR is equal to that constant utility value + else + ϵ = minimum(filter(!iszero, abs.(u_diff))) / 2 + end # Variables and constraints η = @variable(model) From ead57180c44077e84b50c15d734c913785180f3f Mon Sep 17 00:00:00 2001 From: solliolli Date: Fri, 22 Oct 2021 08:38:47 +0300 Subject: [PATCH 133/133] Added contribution guidelines --- CONTRIBUTING.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..8a68b695 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,16 @@ +## Contribution guidelines + +First of all, thank you for your interest in this project! Most of us working on this are researchers, not software engineers, so we expect there to be room for improvement in this package. If you find something that is unclear or doesn't work or should be done more efficiently etc., please let us know, but remember to be respectful. + +If you are new to Julia package development, we strongly recommend reading the rather well-written guide in [the JuMP documentation](https://jump.dev/JuMP.jl/dev/developers/contributing/#Contribute-code-to-JuMP). + +How to proceed when you have: +- A question about how something works + - Ask a question on our [discussion forum](https://github.com/gamma-opt/DecisionProgramming.jl/discussions) + - If the reason for your confusion was that something was not properly explained in the documentation, create an issue and/or a pull request. +- A bug report 🐛 + - Create an issue with a minimal working example that shows how you encountered the bug. + - If you know how to fix the bug, you can create a pull request as well, otherwise we'll see your issue and start working on fixing whatever you found. +- An improvement suggestion + - It might be a good idea to first discuss your idea with us, you can start by posting on the [discussion forum](https://github.com/gamma-opt/DecisionProgramming.jl/discussions). + - Create an issue and start working on a pull request.