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. 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/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 ) diff --git a/docs/src/api.md b/docs/src/api.md index c04f7d19..20edc536 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -5,21 +5,22 @@ ### 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 +34,10 @@ AbstractPathProbability DefaultPathProbability ``` -### Consequences +### Utilities ```@docs -Consequences +Utility +Utilities ``` ### Path Utility @@ -44,6 +46,20 @@ AbstractPathUtility DefaultPathUtility ``` +### InfluenceDiagram +```@docs +InfluenceDiagram +generate_arcs! +generate_diagram! +add_node! +ProbabilityMatrix +add_probabilities! +UtilityMatrix +add_utilities! +index_of +num_states +``` + ### Decision Strategy ```@docs LocalDecisionStrategy @@ -56,15 +72,13 @@ DecisionStrategy ```@docs DecisionVariables PathCompatibilityVariables -lazy_probability_cut(::Model, ::PathCompatibilityVariables, ::AbstractPathProbability) +lazy_probability_cut ``` ### 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 +90,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` @@ -96,9 +111,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) +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/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..fde8d78a 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) 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 146c13c4..dbc3053c 100644 --- a/docs/src/decision-programming/influence-diagram.md +++ b/docs/src/decision-programming/influence-diagram.md @@ -6,17 +6,19 @@ 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. -We define the **information set** $I$ of node $jโˆˆN$ as +The connections between different nodes (arrows) are called **arcs** $a \in A$. The arcs represent different dependencies between the nodes. -$$I(j)โІ\{iโˆˆCโˆชDโˆฃi 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;]] ``` + 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)) +add_node!(diagram, ChanceNode("R2", ["R1", "H", "T2"], R_states)) +add_node!(diagram, ChanceNode("H", ["R0"], H_states)) -Next, we define the nodes with their information sets and corresponding probabilities for chance nodes and consequences for value nodes. +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 they map their information states to utility values instead. -### Prior risk estimate and health of the patient +```julia +add_node!(diagram, ValueNode("TC", ["T1", "T2"])) +add_node!(diagram, ValueNode("HB", ["H", "TD"])) +``` -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 +### 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) +``` + +### 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 $$โ„™(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 = ProbabilityMatrix(diagram, "R0") X_R0[chosen_risk_level] = 1 -push!(C, ChanceNode(R0, I_R0)) -push!(X, Probabilities(R0, X_R0)) +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,140 +104,110 @@ $$โ„™(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 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 -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") +X_H[:, "CHD"] = data.risk_levels +X_H[:, "no CHD"] = 1 .- data.risk_levels +add_probabilities!(diagram, "H", X_H) ``` -### Test decisions and updating the risk estimate +### Probabilities of the updated the risk estimates -The node representing the first test decision is added to the model. - -```julia -I_T1 = [R0] -push!(D, DecisionNode(T1, I_T1)) -``` - -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```. -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 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 -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. +### Utilities of testing costs and health benefits -```julia -I_TD = [R2] -push!(D, DecisionNode(TD, I_TD)) -``` - -### 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") +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) + ``` -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") +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) ``` -### 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 the 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 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 -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) ``` @@ -258,72 +225,85 @@ optimize!(model) -## Analyzing Results - -### Decision Strategy -We obtain the optimal decision strategy from the z variable values. +## Analyzing results +We extract 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 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 -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 ``` -### Utility Distribution +### Utility distribution 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/docs/src/examples/contingent-portfolio-programming.md b/docs/src/examples/contingent-portfolio-programming.md index c7944a28..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 @@ -30,33 +34,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 +49,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 +61,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 +71,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 - -```julia -validate_influence_diagram(S, C, D, V) -sort!.((C, D, V, X, Y), by = x -> x.j) -``` +### 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 -P = DefaultPathProbability(C, X) +generate_diagram!(diagram, default_utility=false) ``` ## Decision Model: Portfolio Selection @@ -123,7 +86,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 +99,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 +113,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 +123,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 +161,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 +201,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 +220,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 +246,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 +298,7 @@ julia> print_utility_distribution(udist) ``` ```julia-repl -julia> print_statistics(udist) +julia> print_statistics(U_distribution) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Name โ”‚ Statistics โ”‚ โ”‚ String โ”‚ Float64 โ”‚ diff --git a/docs/src/examples/n-monitoring.md b/docs/src/examples/n-monitoring.md index 4ff0145e..aafc75ae 100644 --- a/docs/src/examples/n-monitoring.md +++ b/docs/src/examples/n-monitoring.md @@ -6,9 +6,9 @@ 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 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] +``` + +### Initialising the 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. +### 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$. -$$โ„™(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 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 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]...])) +``` + +### 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) +``` + + +### 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 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 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") + 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 ``` -### 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)$ 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$. -$$โ„™(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 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. -$$โ„™(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$ +After declaring the probability matrix, we add it 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$ 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 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 -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,93 +202,88 @@ 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) -``` +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. -```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/docs/src/examples/pig-breeding.md b/docs/src/examples/pig-breeding.md index 446825ee..425df520 100644 --- a/docs/src/examples/pig-breeding.md +++ b/docs/src/examples/pig-breeding.md @@ -1,57 +1,76 @@ -# Pig Breeding +# [Pig Breeding](@id pig-breeding) ## 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. +> 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 decision programming, we start by defining the node indices and states, as follows: +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 -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}() +``` +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 corresponding probabilities or consequences. -### 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. +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$. -The probability that pig is ill in the first month is + +```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 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 + # 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, 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 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) +``` + +### 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 the pig is ill in the first month is $$โ„™(h_1 = ill)=0.1.$$ @@ -59,159 +78,106 @@ 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 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 -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: - -$$โ„™(h_k = ill โˆฃ d_{k-1} = pass, h_{k-1} = healthy)=0.2,$$ +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} = treat, h_{k-1} = healthy)=0.1,$$ +$$โ„™(h_k = ill โˆฃ h_{k-1} = healthy, \ d_{k-1} = pass)=0.2,$$ -$$โ„™(h_k = ill โˆฃ d_{k-1} = pass, h_{k-1} = ill)=0.9,$$ +$$โ„™(h_k = ill โˆฃ h_{k-1} = healthy, \ d_{k-1} = treat)=0.1,$$ -$$โ„™(h_k = ill โˆฃ d_{k-1} = treat, h_{k-1} = ill)=0.5.$$ +$$โ„™(h_k = ill โˆฃ h_{k-1} = ill, \ d_{k-1} = pass)=0.9,$$ -In decision programming: +$$โ„™(h_k = ill โˆฃ h_{k-1} = ill, \ d_{k-1} = treat)=0.5.$$ +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 -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") +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] ``` -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 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 -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") +X_T["ill", "positive"] = 0.8 +X_T["ill", "negative"] = 0.2 +X_T["healthy", "negative"] = 0.9 +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 using a for-loop. ```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 -The cost of treatment decision for the pig at month $k=1,...,N-1$ is defined +### Utilities + +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: +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, 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 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 -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 +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. -```julia -validate_influence_diagram(S, C, D, V) -sort!.((C, D, V, X, Y), by = x -> x.j) -``` +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. -We define the path probability. ```julia -P = DefaultPathProbability(C, X) +generate_diagram!(diagram, positive_path_utility = true) ``` -As the path utility, we use the default, which is the sum of the consequences given the path. -```julia -U = DefaultPathUtility(V, Y) -``` - - -## Decision Model +## 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 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 -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) ``` @@ -227,88 +193,92 @@ optimize!(model) ``` -## Analyzing Results -### Decision Strategy +## Analyzing results -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 +### 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 strategy $Z$ tell the probability of each state in each node, given 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 โ”‚ ill โ”‚ healthy โ”‚ 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 โ”‚ positive โ”‚ negative โ”‚ 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 โ”‚ treat โ”‚ pass โ”‚ 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. +### Utility distribution -```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 +295,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/docs/src/examples/used-car-buyer.md b/docs/src/examples/used-car-buyer.md index f1a0a697..a2979c3f 100644 --- a/docs/src/examples/used-car-buyer.md +++ b/docs/src/examples/used-car-buyer.md @@ -15,130 +15,127 @@ 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}() +diagram = InfluenceDiagram() ``` -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. - -### Car's State +### 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 +### 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 +### 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 +### Purchase 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 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 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 -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") +X_O["peach"] = 0.8 +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$. 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 -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") +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) ``` -### 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$. +### 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 -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") +Y_V1["test"] = -25 +Y_V1["no test"] = 0 +add_utilities!(diagram, "V1", Y_V1) ``` -### Validating the Influence Diagram -Validate influence diagram and sort nodes, probabilities and consequences +We then define the utilities associated with the base profit of the purchase in different scenarios. +```julia +Y_V2 = UtilityMatrix(diagram, "V2") +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 -validate_influence_diagram(S, C, D, V) -sort!.((C, D, V, X, Y), by = x -> x.j) +Y_V3 = UtilityMatrix(diagram, "V3") +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) ``` -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. +## 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() -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) ``` @@ -154,41 +151,36 @@ optimize!(model) ``` -## Analyzing Results -### Decision Strategy -Once the model is solved, we obtain the following decision strategy: +## Analyzing results +Once the model is solved, we extract the results. ```julia Z = DecisionStrategy(z) +S_probabilities = StateProbabilities(diagram, Z) +U_distribution = UtilityDistribution(diagram, Z) ``` -```julia-repl -julia> print_decision_strategy(S, Z) -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ” -โ”‚ Nodes โ”‚ () โ”‚ 2 โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”ค -โ”‚ States โ”‚ () โ”‚ 2 โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”˜ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ” -โ”‚ Nodes โ”‚ (3,) โ”‚ 4 โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”ค -โ”‚ States โ”‚ (1,) โ”‚ 3 โ”‚ -โ”‚ States โ”‚ (2,) โ”‚ 2 โ”‚ -โ”‚ States โ”‚ (3,) โ”‚ 1 โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”˜ -``` - -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) -``` +### Decision strategy +We obtain the following optimal decision strategy: +```julia-repl +julia> print_decision_strategy(diagram, Z, S_probabilities) +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Decision in T โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ test โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ State(s) of R โ”‚ Decision in A โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ lemon โ”‚ buy with guarantee โ”‚ +โ”‚ peach โ”‚ buy without guarantee โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Utility distribution ```julia-repl -julia> print_utility_distribution(udist) +julia> print_utility_distribution(U_distribution) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Utility โ”‚ Probability โ”‚ โ”‚ Float64 โ”‚ Float64 โ”‚ @@ -201,7 +193,7 @@ julia> print_utility_distribution(udist) 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 โ”‚ 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/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/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/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 diff --git a/docs/src/index.md b/docs/src/index.md index 90dfaf79..5e1a2539 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,9 +1,9 @@ # DecisionProgramming.jl -DecisionProgramming.jl is a Julia package for solving *multi-stage decision problems under uncertainty*, modeled using influence diagrams, and leveraging the power of mixed-integer linear programming. Solving multi-stage decision problems under uncertainty consists of the following three steps. +DecisionProgramming.jl is a Julia package for solving *multi-stage decision problems under uncertainty*, modeled using influence diagrams, and leveraging the power of mixed-integer linear programming. The Decision Programming approach to solving multi-stage decision problems under uncertainty consists of the following three steps. In the first step, we model the decision problem using an influence diagram with associated probabilities, consequences, and path utility function. -In the second step, we create a decision model with an objective for the influence diagram. We solve the model to obtain an optimal decision strategy. We can create and solve multiple models with different objectives for the same influence diagram to receive various optimal decision strategies. +In the second step, we create a decision model corresponding to the influence diagram using a suitable objective function. We solve the model to obtain an optimal decision strategy. We can create and solve multiple models with different objectives for the same influence diagram to receive various optimal decision strategies. In the third step, we analyze the resulting decision strategies for the influence diagram. In particular, we are interested in the utility distribution and its associated statistics and risk measures. diff --git a/docs/src/usage.md b/docs/src/usage.md index c63393aa..00872bc2 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -1,61 +1,261 @@ # 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 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 -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"])) ``` +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. -## Decision Nodes -![](figures/decision-node.svg) +```julia +add_node!(diagram, ValueNode("V", ["C2"])) +``` -Given the above influence diagram, we can create the `DecisionNode` structure for the node `3` as follows: +Once all the nodes are added, we generate the arcs. This orders the nodes and numbers them such that each node's predecessors have smaller numbers than the node itself. 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 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 -S = States([2, 3, 2]) -j = 3 -I_j = Node[1, 2] -DecisionNode(j, I_j) +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)})$$ -## Value Nodes -![](figures/value-node.svg) +for all $s_j \in S_j$ and $๐ฌ_{I(j)} \in ๐’_{I(j)}$. -Given the above influence diagram, we can create `ValueNode` and `Consequences` structures for node `3` as follows: +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 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 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"])) +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 -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) +julia> diagram.X +1-element Array{Probabilities,1}: + [0.1, 0.3, 0.6] ``` + + +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}: + "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 $(|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] = ... +X_C2[1, 1, 2] = ... +X_C2[1, 1, 2] = ... +โ‹ฎ +add_probabilities!(diagram, "C2", X_C2) +``` +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}: + ["a", "b"] + ["x", "y", "z"] + ["v", "w"] +``` +### Helper 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 +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 + +julia> size(X_C2) +(2, 3, 2) +``` +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> X_C2["a", "z", "w"] = 0.25 +0.25 + +julia> X_C2["z", "a", "v"] = 0.75 +ERROR: DomainError with Node D1 does not have a state called z.: + +julia> X_C2["a", "z", "v"] = 0.75 +0.75 + +julia> 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. The following achieves the same as what was done above. +```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 +``` + +Now, the probability matrix X_C2 is partially filled. +```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 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 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. + +```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) +``` + + +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> Y_V["w"] = 400 +400 + +julia> 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] +``` + +## 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 +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 d1c34561..f1e38abd 100644 --- a/examples/CHD_preventative_care.jl +++ b/examples/CHD_preventative_care.jl @@ -6,14 +6,13 @@ 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) - # Bayes posterior risk probabilities calculation function # prior = prior risk level for which the posterior risk distribution is calculated for, # t = test done @@ -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"] - -@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}() - - -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)) +const R_states = [string(x) * "%" for x in [0:1:100;]] -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)) +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_T1 = [R0] -push!(D, DecisionNode(T1, I_T1)) +add_node!(diagram, ValueNode("TC", ["T1", "T2"])) +add_node!(diagram, ValueNode("HB", ["H", "TD"])) -I_R1 = [R0, H, T1] -X_R1 = zeros(S[I_R1]..., S[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) -end -push!(C, ChanceNode(R1, I_R1)) -push!(X, Probabilities(R1, X_R1)) +generate_arcs!(diagram) +X_R0 = ProbabilityMatrix(diagram, "R0") +X_R0[chosen_risk_level] = 1 +add_probabilities!(diagram, "R0", X_R0) -I_T2 = [R1] -push!(D, DecisionNode(T2, I_T2)) +X_H = ProbabilityMatrix(diagram, "H") +X_H[:, "CHD"] = data.risk_levels +X_H[:, "no CHD"] = 1 .- data.risk_levels +add_probabilities!(diagram, "H", X_H) -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) +X_R = ProbabilityMatrix(diagram, "R1") +for s_R0 = 1:101, s_H = 1:2, s_T1 = 1:3 + 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(R2, I_R2)) -push!(X, Probabilities(R2, X_R2)) +add_probabilities!(diagram, "R1", X_R) +add_probabilities!(diagram, "R2", X_R) -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") +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") +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) @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,22 @@ optimizer = optimizer_with_attributes( "MIPGap" => 1e-6, ) set_optimizer(model, optimizer) -optimize!(model) +optimize!(model) @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) diff --git a/examples/contingent-portfolio-programming.jl b/examples/contingent-portfolio-programming.jl index 8d4baa2b..13489680 100644 --- a/examples/contingent-portfolio-programming.jl +++ b/examples/contingent-portfolio-programming.jl @@ -4,43 +4,23 @@ 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แต€]) +@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 +30,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") @@ -78,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 @@ -85,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(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 -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) M = 20 # a large constant ฮต = 0.5*minimum([O_t O_a]) # a helper variable, allows using โ‰ค instead of < in constraints (28b) and (29b) @@ -100,35 +78,36 @@ 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: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 )) + @info("Starting the optimization process.") optimizer = optimizer_with_attributes( @@ -141,29 +120,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_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.") -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) 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 diff --git a/examples/n_monitoring.jl b/examples/n_monitoring.jl index 7ec29b3e..364ad8b9 100644 --- a/examples/n_monitoring.jl +++ b/examples/n_monitoring.jl @@ -5,97 +5,69 @@ 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"] 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 +diagram = InfluenceDiagram() + +add_node!(diagram, ChanceNode("L", [], ["high", "low"])) -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)) +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 -for (i, j) in zip(R_k, A_k) - I_j = [i] - push!(D, DecisionNode(j, I_j)) +add_node!(diagram, ChanceNode("F", ["L", ["A$i" for i in 1:N]...], ["failure", "success"])) + +add_node!(diagram, ValueNode("T", ["F", ["A$i" for i in 1:N]...])) + +generate_arcs!(diagram) + +X_L = [rand(), 0] +X_L[2] = 1.0 - X_L[1] +add_probabilities!(diagram, "L", X_L) + +for i in 1:N + x_R, y_R = rand(2) + X_R = ProbabilityMatrix(diagram, "R$i") + 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 -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 = 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) -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)) + +Y_T = UtilityMatrix(diagram, "T") +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) -validate_influence_diagram(S, C, D, V) -sort!.((C, D, V, X, Y), by = x -> x.j) +generate_diagram!(diagram, positive_path_utility=true) -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) +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.") @@ -108,22 +80,20 @@ optimize!(model) @info("Extracting results.") Z = DecisionStrategy(z) +U_distribution = UtilityDistribution(diagram, Z) +S_probabilities = StateProbabilities(diagram, 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) +print_decision_strategy(diagram, Z, S_probabilities) -@info("Computing utility distribution.") -udist = UtilityDistribution(S, P, U, Z) +@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(udist) +print_utility_distribution(U_distribution) @info("Printing statistics") -print_statistics(udist) +print_statistics(U_distribution) diff --git a/examples/pig_breeding.jl b/examples/pig_breeding.jl index bf26415c..e8acde4d 100644 --- a/examples/pig_breeding.jl +++ b/examples/pig_breeding.jl @@ -3,97 +3,61 @@ 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)) +diagram = InfluenceDiagram() + +add_node!(diagram, ChanceNode("H1", [], ["ill", "healthy"])) +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 - -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)) +add_node!(diagram, ValueNode("MP", ["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") +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") +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) + add_probabilities!(diagram, "H$(i+1)", X_H) 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)) +for i in 1:N-1 + add_utilities!(diagram, "C$i", [-100.0, 0.0]) 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)) -end +add_utilities!(diagram, "MP", [300.0, 1000.0]) -validate_influence_diagram(S, C, D, V) -sort!.((C, D, V, X, Y), by = x -> x.j) +generate_diagram!(diagram, positive_path_utility = true) -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) +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.") @@ -106,30 +70,28 @@ 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("Printing utility distribution.") +print_utility_distribution(U_distribution) + +@info("Printing statistics") +print_statistics(U_distribution) @info("State probabilities:") -sprobs = StateProbabilities(S, 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(S, 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 - -@info("Computing utility distribution.") -udist = UtilityDistribution(S, P, U, Z) - -@info("Printing utility distribution.") -print_utility_distribution(udist) - -@info("Printing statistics") -print_statistics(udist) diff --git a/examples/used_car_buyer.jl b/examples/used_car_buyer.jl index 5ce895ef..0ad7f140 100644 --- a/examples/used_car_buyer.jl +++ b/examples/used_car_buyer.jl @@ -1,76 +1,60 @@ + using Logging 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"] - @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[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)) - -I_V2 = [A] -Y_V2 = [100.0, 40.0, 0.0] -push!(V, ValueNode(6, I_V2)) -push!(Y, Consequences(6, Y_V2)) - -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) - -P = DefaultPathProbability(C, X) -U = DefaultPathUtility(V, Y) +diagram = InfluenceDiagram() + +add_node!(diagram, ChanceNode("O", [], ["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"])) +add_node!(diagram, ValueNode("V2", ["A"])) +add_node!(diagram, ValueNode("V3", ["O", "A"])) + +generate_arcs!(diagram) + +X_O = ProbabilityMatrix(diagram, "O") +X_O["peach"] = 0.8 +X_O["lemon"] = 0.2 +add_probabilities!(diagram, "O", X_O) + +X_R = ProbabilityMatrix(diagram, "R") +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") +Y_V1["test"] = -25 +Y_V1["no test"] = 0 +add_utilities!(diagram, "V1", Y_V1) + +Y_V2 = UtilityMatrix(diagram, "V2") +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") +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) + @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.") @@ -83,15 +67,14 @@ 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) - -@info("Computing utility distribution.") -udist = UtilityDistribution(S, P, U, Z) +print_decision_strategy(diagram, Z, S_probabilities) @info("Printing utility distribution.") -print_utility_distribution(udist) +print_utility_distribution(U_distribution) @info("Printing expected utility.") -print_statistics(udist) +print_statistics(U_distribution) diff --git a/src/DecisionProgramming.jl b/src/DecisionProgramming.jl index af1b4e17..3636b2d2 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, @@ -15,31 +16,45 @@ export Node, States, Path, paths, + ForbiddenPath, + FixedPath, Probabilities, - Consequences, + Utility, + Utilities, AbstractPathProbability, DefaultPathProbability, AbstractPathUtility, DefaultPathUtility, + validate_influence_diagram, + InfluenceDiagram, + generate_arcs!, + generate_diagram!, + index_of, + num_states, + add_node!, + ProbabilityMatrix, + add_probabilities!, + UtilityMatrix, + add_utilities!, LocalDecisionStrategy, - DecisionStrategy, - validate_influence_diagram + DecisionStrategy export DecisionVariables, PathCompatibilityVariables, - ForbiddenPath, lazy_probability_cut, - PositivePathUtility, - NegativePathUtility, expected_value, - value_at_risk, conditional_value_at_risk -export random_diagram +export random_diagram!, + random_probabilities!, + random_utilities!, + LocalDecisionStrategy export CompatiblePaths, UtilityDistribution, - StateProbabilities + StateProbabilities, + value_at_risk, + conditional_value_at_risk export print_decision_strategy, print_utility_distribution, diff --git a/src/analysis.jl b/src/analysis.jl index bbb8a4e0..21bb6754 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -1,12 +1,20 @@ +""" + struct CompatiblePaths + S::States + C::Vector{Node} + Z::DecisionStrategy + fixed::FixedPath + end +CompatiblePaths type. +""" struct CompatiblePaths S::States - C::Vector{ChanceNode} + C::Vector{Node} Z::DecisionStrategy - fixed::Dict{Node, State} + fixed::FixedPath 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) @@ -14,9 +22,9 @@ struct CompatiblePaths end """ - CompatiblePaths(S::States, C::Vector{ChanceNode}, 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)` @@ -24,57 +32,58 @@ 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(S::States, C::Vector{ChanceNode}, Z::DecisionStrategy) - CompatiblePaths(S, C, 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{ChanceNode}, Z::DecisionStrategy, s_C::Path) - s = Array{Int}(undef, length(S)) +function compatible_path(S::States, C::Vector{Node}, Z::DecisionStrategy, s_C::Path) + s = Array{State}(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{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) 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 + struct UtilityDistribution + u::Vector{Float64} + p::Vector{Float64} + end UtilityDistribution type. @@ -85,23 +94,23 @@ 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) +UtilityDistribution(diagram, 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(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) - utilities[i] = U(s) - probabilities[i] = P(s) + utilities[i] = diagram.U(s) + probabilities[i] = diagram.P(s) end # Filter zero probabilities @@ -132,84 +141,112 @@ function UtilityDistribution(S::States, P::AbstractPathProbability, U::AbstractP 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 """ - function StateProbabilities(S::States, P::AbstractPathProbability, Z::DecisionStrategy, node::Node, state::State, prev::StateProbabilities) + StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy, node::Node, state::State, prior_probabilities::StateProbabilities) -Associates each node with array of conditional probabilities for each of its states occuring in active 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 indices. # Examples ```julia # Prior probabilities -prev = StateProbabilities(S, P, Z) - -# Select node and fix its state -node = 1 -state = 2 -StateProbabilities(S, P, Z, node, state, prev) +prior_probabilities = StateProbabilities(diagram, Z) +StateProbabilities(diagram, Z, Node(2), State(1), prior_probabilities) ``` """ -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 = deepcopy(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 end StateProbabilities(probs, fixed) end """ - function StateProbabilities(S::States, P::AbstractPathProbability, Z::DecisionStrategy) + 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. Fix node and state using their names. + +# Examples +```julia +# Prior probabilities +prior_probabilities = StateProbabilities(diagram, Z) + +# Select node and fix its state +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) + node_index = findfirst(j -> j ==node, diagram.Names) + state_index = findfirst(j -> j == state, diagram.States[node_index]) + + return StateProbabilities(diagram, Z, Node(node_index), State(state_index), prior_probabilities) +end + + + + +""" + StateProbabilities(diagram::InfluenceDiagram, Z::DecisionStrategy) -Associates each node with array of probabilities for each of its states occuring in active paths. +Associate each node with array of probabilities for each of its states occuring in compatible paths. # Examples ```julia -StateProbabilities(S, P, Z) +StateProbabilities(diagram, 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 """ - 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::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 """ - function conditional_value_at_risk(u::Vector{Float64}, p::Vector{Float64}, ฮฑ::Float64) + conditional_value_at_risk(U_distribution::UtilityDistribution, ฮฑ::Float64) -Conditional value-at-risk. +Calculate 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 diff --git a/src/decision_model.jl b/src/decision_model.jl index 52d64def..5ade4415 100644 --- a/src/decision_model.jl +++ b/src/decision_model.jl @@ -1,44 +1,44 @@ 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} + I_d::Vector{Vector{Node}} z::Vector{<:Array{VariableRef}} 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) +z = DecisionVariables(model, diagram) ``` """ -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, 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}) @@ -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) @@ -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) @@ -69,215 +70,157 @@ 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 """ PathCompatibilityVariables(model::Model, - z::DecisionVariables, - S::States, - P::AbstractPathProbability; + diagram::InfluenceDiagram, + z::DecisionVariables; names::Bool=false, name::String="x", forbidden_paths::Vector{ForbiddenPath}=ForbiddenPath[], - fixed::Dict{Node, State}=Dict{Node, State}(), - probability_cut::Bool=true) + fixed::FixedPath=Dict{Node, State}(), + probability_cut::Bool=true, + probability_scale_factor::Float64=1.0) 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. 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. +- `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 -x_s = PathCompatibilityVariables(model, z, S, P; probability_cut = false) +x_s = PathCompatibilityVariables(model, diagram; 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[], - fixed::Dict{Node, State}=Dict{Node, State}(), - probability_cut::Bool=true) + fixed::FixedPath=Dict{Node, State}(), + 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 # 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) + 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) ) 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, diagram.I_j[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) * probability_scale_factor for (s, x) in x_s) == 1.0 * probability_scale_factor) end x_s 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) +lazy_probability_cut(model, diagram, x_s) ``` !!! note 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 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 - 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 - 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, - U::AbstractPathUtility, - P::AbstractPathProbability; - probability_scale_factor::Float64=1.0) + diagram::InfluenceDiagram, + x_s::PathCompatibilityVariables) 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) +EV = expected_value(model, diagram, x_s) ``` """ function expected_value(model::Model, - x_s::PathCompatibilityVariables, - U::AbstractPathUtility, - P::AbstractPathProbability; - probability_scale_factor::Float64=1.0) + diagram::InfluenceDiagram, + x_s::PathCompatibilityVariables) - 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, diagram.translation) for (s, x) in x_s)) 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 @@ -285,10 +228,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. @@ -303,9 +244,8 @@ 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 @@ -316,18 +256,18 @@ 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(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] 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) @@ -335,7 +275,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) @@ -349,14 +289,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) / (ฮฑ * probability_scale_factor)) return CVaR end @@ -368,8 +308,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 """ @@ -383,5 +323,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, 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 fc17e339..c0d69bff 100644 --- a/src/influence_diagram.jl +++ b/src/influence_diagram.jl @@ -4,11 +4,18 @@ using Base.Iterators: product # --- Nodes and States --- """ - Node = Int + Node = Int16 -Primitive type for node. Alias for `Int`. +Primitive type for node index. Alias for `Int16`. """ -const Node = Int +const Node = Int16 + +""" + Name = String + +Primitive type for node names. Alias for `String`. +""" +const Name = String """ abstract type AbstractNode end @@ -17,88 +24,71 @@ 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 + struct ChanceNode <: AbstractNode -Chance node type. - -# Examples -```julia -c = ChanceNode(3, [1, 2]) -``` +A struct for chance nodes, includes the name, information set and states of the node """ 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) + name::Name + I_j::Vector{Name} + states::Vector{Name} + function ChanceNode(name, I_j, states) + return new(name, I_j, states) end + end """ - DecisionNode <: AbstractNode - -Decision node type. + struct DecisionNode <: AbstractNode -# Examples -```julia -d = DecisionNode(2, [1]) -``` +A struct for decision nodes, includes the name, information set and states of the node """ 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) + name::Name + I_j::Vector{Name} + states::Vector{Name} + function DecisionNode(name, I_j, states) + return new(name, I_j, states) end end """ - ValueNode <: AbstractNode + struct ValueNode <: AbstractNode -Value node type. - -# Examples -```julia -v = ValueNode(4, [1, 3]) -``` +A struct for value nodes, includes the name and information set of the node """ 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) + name::Name + I_j::Vector{Name} + function ValueNode(name, I_j) + return new(name, I_j) end 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 """ - States <: AbstractArray{State, 1} + struct States <: AbstractArray{State, 1} States type. Works like `Vector{State}`. # Examples -```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} @@ -117,68 +107,55 @@ Base.getindex(S::States, i::Int) = getindex(S.vals, i) Base.length(S::States) = length(S.vals) Base.eltype(S::States) = eltype(S.vals) + + +# --- Paths --- + +""" + const Path{N} = NTuple{N, State} where N + +Path type. Alias for `NTuple{N, State} where N`. """ - function States(states::Vector{Tuple{State, Vector{Node}}}) +const Path{N} = NTuple{N, State} where N -Construct states from vector of (state, nodes) tuples. + +""" + const ForbiddenPath = Tuple{Vector{Node}, Set{Path}} + +ForbiddenPath type. # Examples ```julia-repl -julia> S = States([(2, [1, 3]), (3, [2, 4, 5])]) -States([2, 3, 2, 3, 3]) +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)])) ``` """ -function States(states::Vector{Tuple{State, Vector{Node}}}) - 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 +const ForbiddenPath = Tuple{Vector{Node}, Set{Path}} -""" - function validate_influence_diagram(S::States, C::Vector{ChanceNode}, D::Vector{DecisionNode}, V::Vector{ValueNode}) -Validate influence diagram. """ -function validate_influence_diagram(S::States, C::Vector{ChanceNode}, D::Vector{DecisionNode}, V::Vector{ValueNode}) - n = length(C) + length(D) - if length(S) != n - throw(DomainError("Each change and decision node should have states.")) - end - 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 - 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 - 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 - # 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 - for v in V - if isempty(v.I_j) - @warn("Value node $(v.j) is redundant.") - end - end -end + const FixedPath = Dict{Node, State} +FixedPath type. -# --- Paths --- - +# Examples +```julia-repl +julia> FixedPath(Dict(1=>1, 2=>3)) +Dict{Int16,Int16} with 2 entries: + 2 => 3 + 1 => 1 +``` """ - const Path{N} = NTuple{N, State} where N +const FixedPath = Dict{Node, State} -Path type. Alias for `NTuple{N, State} where N`. -""" -const Path{N} = NTuple{N, State} where N """ function paths(states::AbstractVector{State}) @@ -187,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}) @@ -197,18 +184,25 @@ 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)))) -[(2, 1), (2, 2), (2, 3)] +julia> states = States(State.([2, 3])) +2-element States: + 2 + 3 + +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::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) @@ -216,48 +210,43 @@ function paths(states::AbstractVector{State}, fixed::Dict{Node, State}) product(iters...) end -""" - const ForbiddenPath = Tuple{Vector{Node}, Set{Path}} - -ForbiddenPath type. - -# Examples -```julia -ForbiddenPath[ - ([1, 2], Set([(1, 2)])), - ([3, 4, 5], Set([(1, 2, 3), (3, 4, 5)])) -] -``` -""" -const ForbiddenPath = Tuple{Vector{Node}, Set{Path}} - # --- Probabilities --- """ 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] -julia> X = Probabilities(2, data) +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 ``` """ 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 @@ -275,68 +264,107 @@ Base.getindex(P::Probabilities, I::Vararg{Int,N}) where N = getindex(P.data, I.. abstract type AbstractPathProbability end Abstract path probability type. - -# Examples -```julia -struct PathProbability <: AbstractPathProbability - C::Vector{ChanceNode} - # ... -end - -(U::PathProbability)(s::Path) = ... -``` """ abstract type AbstractPathProbability end """ - DefaultPathProbability <: AbstractPathProbability + 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 -P = DefaultPathProbability(C, X) -s = (1, 2) -P(s) +```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 - C::Vector{ChanceNode} + C::Vector{Node} + I_c::Vector{Vector{Node}} X::Vector{Probabilities} + 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 + 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_c; c]]) for (c, I_c, X) in zip(P.C, P.I_c, P.X)) end -# --- Consequences --- +# --- Utilities --- + +""" + const Utility = Float32 +Primitive type for utility. Alias for `Float32`. """ - Consequences{N} <: AbstractArray{Float64, N} +const Utility = Float32 + +""" + struct 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> 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 Consequences{N} <: AbstractArray{Float64, N} - j::Node - data::Array{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 --- @@ -345,58 +373,779 @@ Base.getindex(Y::Consequences, I::Vararg{Int,N}) where N = getindex(Y.data, I... abstract type AbstractPathUtility end Abstract path utility type. +""" +abstract type AbstractPathUtility end + +""" + struct DefaultPathUtility <: AbstractPathUtility + +Default path utility obtained as a sum of the utility values corresponding to path s in each value node. + +# Examples +```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> U(s, t) +-102.0f0 +``` +""" +struct DefaultPathUtility <: AbstractPathUtility + I_v::Vector{Vector{Node}} + 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::Utility) + U(s) + t +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 -struct PathUtility <: AbstractPathUtility - V::Vector{ValueNode} - # ... +diagram = InfluenceDiagram() +``` +""" +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 -(U::PathUtility)(s::Path) = ... + +# --- Adding nodes --- + +function validate_node(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 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 + +""" + function add_node!(diagram::InfluenceDiagram, node::AbstractNode) + +Add node to influence diagram structure. + +# Examples +```julia-repl +julia> add_node!(diagram, ChanceNode("O", [], ["lemon", "peach"])) +1-element Array{AbstractNode,1}: + ChanceNode("O", String[], ["lemon", "peach"]) ``` """ -abstract type AbstractPathUtility end +function add_node!(diagram::InfluenceDiagram, node::AbstractNode) + if !isa(node, ValueNode) + validate_node(diagram, node.name, node.I_j, states = node.states) + else + validate_node(diagram, node.name, node.I_j, value_node = true) + end + push!(diagram.Nodes, node) +end + +# --- Adding Probabilities --- """ - DefaultPathUtility <: AbstractPathUtility + struct ProbabilityMatrix{N} <: AbstractArray{Float64, N} + nodes::Vector{Name} + indices::Vector{Dict{Name, Int}} + matrix::Array{Float64, N} + end -Default path utility. +Construct probability matrix. +""" +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...) +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) + +Initialise a probability matrix for a given chance node. The matrix is initialised with zeros. + +# Examples +```julia-repl +julia> X_O = ProbabilityMatrix(diagram, "O") +2-element ProbabilityMatrix{1}: + 0.0 + 0.0 +``` +""" +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 = fill(0.0, diagram.S[nodes]...) + + return ProbabilityMatrix(names, indices, matrix) +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 -U = DefaultPathUtility(V, Y) -s = (1, 2) -U(s) +julia> X_O = ProbabilityMatrix(diagram, "O") +2-element ProbabilityMatrix{1}: + 0.0 + 0.0 + +julia> X_O["lemon"] = 0.2 +0.2 + +julia> add_probabilities!(diagram, "O", X_O) +ERROR: DomainError with Probabilities should sum to one.: + +julia> X_O["peach"] = 0.8 +0.2 + +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. """ -struct DefaultPathUtility <: AbstractPathUtility - V::Vector{ValueNode} - Y::Vector{Consequences} +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 + push!(diagram.X, Probabilities(Node(c), probabilities.matrix)) + else + 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)).")) + end end -function (U::DefaultPathUtility)(s::Path) - sum(Y(s[v.I_j]) for (v, Y) in zip(U.V, U.Y)) + +# --- 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}} + 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...) +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 -# --- Local Decision Strategy --- +""" + function UtilityMatrix(diagram::InfluenceDiagram, node::Name) + +Initialise a utility matrix for a value node. The matrix is initialised with `Inf` values. +# Examples +```julia-repl +julia> Y_V3 = UtilityMatrix(diagram, "V3") +2ร—3 UtilityMatrix{2}: + Inf Inf Inf + Inf Inf Inf +``` """ - LocalDecisionStrategy{N} <: AbstractArray{Int, N} +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 -Local decision strategy type. + # 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 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-repl +julia> Y_V3 = UtilityMatrix(diagram, "V3") +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 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) + + 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)) + else + # Conversion to Float32 using Utility(), since machine default is Float64 + 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)).")) + 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 + 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)) + 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 !isempty(V) && i โˆ‰ union((v.I_j for v in V)...) + @warn("Node $i is redundant.") + end + end +end + +""" + function generate_arcs!(diagram::InfluenceDiagram) + +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 -Z = LocalDecisionStrategy(1, data) -Z(s_I) +generate_arcs!(diagram) ``` """ +function generate_arcs!(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_j = Vector{Vector{Node}}(undef, n_CD+n_V) + states = Vector{Vector{Name}}() + S = Vector{State}(undef, n_CD) + C = Vector{Node}() + D = Vector{Node}() + V = Vector{Node}() + + # Declare helper collections + indices = Dict{Name, Node}() + indexed_nodes = Set{Name}() + # Declare index + index = 1 + + + 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] = Name(j.name) #TODO datatype conversion happens here, should we use push! ? + I_j[index] = map(x -> Node(indices[x]), j.I_j) + push!(states, j.states) + S[index] = State(length(j.states)) + if isa(j, ChanceNode) + push!(C, Node(index)) + else + push!(D, Node(index)) + end + # Increase index + index += 1 + end + + # 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 + + + # Index value nodes + for v in V_nodes + # Update results + 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 + + diagram.Names = Names + diagram.I_j = I_j + diagram.States = states + diagram.S = States(S) + diagram.C = C + diagram.D = D + diagram.V = V + # Declaring X and Y + diagram.X = Vector{Probabilities}() + diagram.Y = Vector{Utilities}() +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 +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, + positive_path_utility::Bool=false, + negative_path_utility::Bool=false) + + + # Sort probabilities and consequences + sort!(diagram.X, by = x -> x.c) + sort!(diagram.Y, by = x -> x.v) + + + # Declare P and U if defaults are used + if default_probability + diagram.P = DefaultPathProbability(diagram.C, diagram.I_j[diagram.C], diagram.X) + end + if default_utility + 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 = 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 + +""" + function index_of(diagram::InfluenceDiagram, node::Name) + +Get the index of a given node. + +# Example +```julia-repl +julia> idx_O = index_of(diagram, "O") +1 +``` +""" +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-repl +julia> NS_O = num_states(diagram, "O") +2 +``` +""" +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 + +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 +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 FixedPath(diagram::InfluenceDiagram, fixed::Dict{Name, Name}) + +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-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}) + 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 --- + +""" + LocalDecisionStrategy{N} <: AbstractArray{Int, N} + +Local decision strategy type. +""" 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 @@ -405,7 +1154,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 @@ -427,6 +1176,7 @@ end Decision strategy type. """ struct DecisionStrategy - D::Vector{DecisionNode} - Z_j::Vector{LocalDecisionStrategy} + D::Vector{Node} + I_d::Vector{Vector{Node}} + Z_d::Vector{LocalDecisionStrategy} end diff --git a/src/printing.jl b/src/printing.jl index 611ef2ff..7f906aaa 100644 --- a/src/printing.jl +++ b/src/printing.jl @@ -2,38 +2,57 @@ 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::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 -print_decision_strategy(S, Z) +print_decision_strategy(diagram, Z, S_probabilities) ``` """ -function print_decision_strategy(S::States, Z::DecisionStrategy) - for (d, Z_j) in zip(Z.D, Z.Z_j) - a1 = vec(collect(paths(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::Bool = 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, 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, header = ["Decision in $(diagram.Names[d])"], alignment=:l) + end end 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) +U_distribution = UtilityDistribution(diagram, Z) +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])) @@ -41,44 +60,52 @@ 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(diagram::InfluenceDiagram, state_probabilities::StateProbabilities, nodes::Vector{Name}; 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]) +S_probabilities = StateProbabilities(diagram, Z) +print_state_probabilities(S_probabilities, ["R"]) +print_state_probabilities(S_probabilities, ["A"]) ``` """ -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] + 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 nodes) - states = 1:limit - df = DataFrame() - df[!, :Node] = nodes - for state in states - df[!, Symbol("State $state")] = [prob(probs[i], state) for i in nodes] + 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 nodes] - pretty_table(df; formatters = ft_printf(prob_fmt, (first(states)+1):(last(states)+1))) 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) @@ -86,14 +113,13 @@ 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 - VaR = [value_at_risk(u, p, ฮฑ) for ฮฑ in ฮฑs] - CVaR = [conditional_value_at_risk(u, p, ฮฑ) for ฮฑ in ฮฑs] +function print_risk_measures(U_distribution::UtilityDistribution, ฮฑs::Vector{Float64}; fmt = "%f") + 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 diff --git a/src/random.jl b/src/random.jl index 77bc0695..7f6d3737 100644 --- a/src/random.jl +++ b/src/random.jl @@ -1,13 +1,14 @@ 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. """ -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,37 @@ 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) + 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. +- `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, [2,3]) ``` """ -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,62 +63,84 @@ 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`. + 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 -# 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)) + 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) -c = ChanceNode(2, [1]) -S = States([2, 2]) -Probabilities(rng, c, S) +diagram = InfluenceDiagram() +random_diagram!(rng, diagram, 5, 2, 3, 2, 2, [2,3]) +c = diagram.C[1] +random_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) + 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] 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,53 +171,65 @@ 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 random_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, [2,3]) +v = diagram.V[1] +random_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 !(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 - 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) + function LocalDecisionStrategy(rng::AbstractRNG, diagram::InfluenceDiagram, d::Node) -Generate random decision strategy for decision node `d` with `S` states. +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, 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 diff --git a/test/decision_model.jl b/test/decision_model.jl index 7a82a782..2aeb82ef 100644 --- a/test/decision_model.jl +++ b/test/decision_model.jl @@ -3,85 +3,86 @@ 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) + 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 PositivePathUtility" - Uโ€ฒ = if probability_cut U else PositivePathUtility(S, U) end + x_s = PathCompatibilityVariables(model, diagram, z; probability_cut = probability_cut, probability_scale_factor = 1.0) @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) - else - @test_throws DomainError expected_value(model, x_s, Uโ€ฒ, P; 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 - 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) - - @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]) + S_probabilities2 = StateProbabilities(diagram, Z, node, state, S_probabilities) + + @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) + 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 +90,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 diff --git a/test/influence_diagram.jl b/test/influence_diagram.jl index 26bf673d..81755e57 100644 --- a/test/influence_diagram.jl +++ b/test/influence_diagram.jl @@ -2,90 +2,186 @@ 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[] + +#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() +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") +X_A["a"] = 0.2 +@test X_A == [0.2, 0] +X_A["b"] = 0.9 +@test X_A == [0.2, 0.9] +@test_throws DomainError add_probabilities!(diagram, "A", X_A) +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) +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]] +@test_throws DomainError add_utilities!(diagram, "V", Y_V) + + +@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) -@info "Testing LocalDecisionStrategy" -@test_throws DomainError LocalDecisionStrategy(1, [0, 0, 2]) -@test_throws DomainError LocalDecisionStrategy(1, [0, 1, 1]) +@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)) 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)