Usage
Here we show you how to construct a simple influence diagram and create a decision model using pyDecisionProgramming.
First we import pyDecisionProgramming and activate the Julia environment.
import DecisionProgramming as dp
dp.activate()
Adding Nodes
We will create the influence diagram pictures above. First we create a new influence diagram.
diagram = dp.InfluenceDiagram()
Next we define each node as a
DecisionNode
, a ChanceNode
or
ValueNode
and add them to the diagram.
Creating a DecisionNode
or a
ChanceNode
requires giving it a unique
name, its information set and its states. If the
node is a root node, its information set is an
empty list ([]
). The order in which the
nodes are added does not matter.
Use the add_node method to add nodes to the diagram.
D1 = dp.DecisionNode("D1", [], ["a", "b"])
diagram.add_node(D1)
C2 = dp.ChanceNode("C2", ["D1", "C1"], ["v", "w"])
diagram.add_node(C2)
C1 = dp.ChanceNode("C1", [], ["x", "y", "z"])
diagram.add_node(C1)
Value nodes only need a name and their information set. They do not have a state, since their purpose is to map their information state to utility values.
V = dp.ValueNode("V", ["C2"])
diagram.add_node(V)
Once all the nodes have been added, we generate the arcs in the diagram. This orders the nodes and numbers them such that each nodes predecessors will have a smaller number than they do. In effect, the change and decision nodes are numbered such that \(C \bigcup D = \{ 1, \dots, n\}\), where \(n=|C|+|D|\). For more details see the page on influence diagrams in the documentation for DecisionProgramming.jl.
diagram.generate_arcs()
The fields Names
, I_j
,
States
, S
, C
, D
and V
in the influence diagram have been
defined. The names field holds the names of
all nodes in the order of they 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. The field I_j
holds the
information sets of each node. Notice, that the
nodes are identified by their numbers. The 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.
In [1]: diagram.Names
Out[1]: ["D1", "C1", "C2", "V"]
In [2]: diagram.I_j
Out[2]: Vector{Int16}[[], [], [1, 2], [3]]
In [3]: diagram.States
Out[3]: [["a", "b"], ["x", "y", "z"], ["v", "w"]]
In [4]: diagram.S
Out[4]: Int16[2, 3, 2]
In [5]: diagram.C
Out[5]: Int16[2, 3]
In [6]: diagram.D
Out[6]: Int16[1]
In [7]: diagram.V
Out[7]: Int16[4]
Probability Matrices
Each change node needs a probability matrix which describes the probability distribution over its states given an information state. It holds probability values \(\mathbb P \left(X_j=s_j | X_{I(j)} = {\bf s}_{I(j)} \right)\) for all \(s_j \in S_j\) and \({\bf s}_{I(j)} \in {\bf S}_{I(j)}\).
Thus, the probability matrix of a chance node needs to have dimensions that correspond to the number of states of the nodes in its information set and number of state of the node itself.
For example, the node C1 in the influence diagram
above has an empty information set and 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, 0.3, 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.
# How C1 was added:
# C1 = dp.ChanceNode("C1", [], ["x", "y", "z"])
# diagram.add_node(C1)
X_C1 = [0.1, 0.3, 0.6]
diagram.set_probabilities("C1", X_C1)
The add_probabilities
function adds the
probability matrix as a Probabilities structure
into the influence diagram’s X
field.
In [8]: diagram.X
Out[8]: Probabilities[[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 \(\left(|S_{C1}|, |S_{D1}|, |S_{C2}|\right) = (3,2,2)\) or \(\left(|S_{D1}|, |S_{C1}|, |S_{C2}|\right) = (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:
In [9]: diagram.Names
Out[9]: ["D1", "C1", "C2", "V"]
In [10]: diagram.I_j
Out[10]: Vector{Int16}[[], [], [1, 2], [3]]
In [11]: diagram.S
Out[11]: Int16[2, 3, 2]
Therefore, the probability matrix of node C2 should have dimensions \(\left(|S_{D1}|, |S_{C1}|, |S_{C2}|\right) = (2,3,2)\). The probability matrix can be added by declaring the matrix and then filling in the probability values as shown below.
import numpy as np
X_C2 = np.zeros([2, 3, 2])
X_C2[1, 1, 1] = ...
X_C2[1, 1, 2] = ...
X_C2[1, 1, 2] = ...
⋮
diagram.add_probabilities("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.
In [12]: diagram.States
Out[12]: [["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
diagram.construct_probability_matrix
function. It initiliases the probability matrix
with zeros.
In [11]: X_C2 = diagram.construct_probability_matrix("C2")
In [13]: X_C2
Out[13]:
[0.0 0.0 0.0; 0.0 0.0 0.0]
[0.0 0.0 0.0; 0.0 0.0 0.0]
In [14]: X_C2.size()
Out[14]: (2, 3, 2)
A matrix of type dp.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
.
X_C2["a", "z", "w"] = 0.25
X_C2["a", "z", "v"] = 0.75
X_C2["a", "x", :] = [0.3, 0.7]
Trying with an incorrect name causes a
JuliaError
to be raised.
In[15]: X_C2["z", "a", "v"] = 0.75
⋮
JuliaError: Exception 'UndefVarError: probability_matrix not defined' occurred while calling julia code:
pyDP74ca39945e["z","a","v"] = 0.75
A matrix of type dp.ProbabilityMatrix
can
also be filled using the matrix indices if that is
more convenient. The following achieves the same
as what was done above.
X_C2[1, 3, 2] = 0.25
X_C2[1, 3, 1] = 0.75
X_C2[1, 1, :] = [0.3, 0.7]
Now, the probability matrix X_C2
is
partially filled.
In[16]: X_C2
Out[16]:
[0.3 0.0 0.75; 0.0 0.0 0.0]
[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
dp.ProbabilityMatrix
.
diagram.set_probabilities("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.
Y_V = np.zeros([2])
Y_V[1] = -100
Y_V[2] = 400
diagram.set_utility("V", Y_V)
The other option is to add the utility matrix
using the
diagram.construct_utility_matrix
function. This is very similar to the
diagram.construct_probability_matrix
function. The
diagram.construct_utility_matrix
function initialises the values to infinity. Using
the diagram.construct_utility_matrix
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.
Y_V = diagram.construct_utility_matrix("V")
Y_V["w"] = -100
Y_V["v"] = 400
diagram.set_utility("V", Y_V)
The diagram.set_utility
function adds the utility matrix into the influence diagram’s Y field.
In [17]: diagram.Y
Out[17]: Utilities[[400.0, -100.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.
diagram.generate()
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({\bf s})\) and path utility
\(\mathbb{U}({\bf s})\)
are defined in the model. By default, the function
will set them to default path probability and
default path utility. See the
the page on influence diagrams in the
documentation for DecisionProgramming.jl
for more information on default path probability
and utility.
Analyzing the Graph
Once the diagram is fully constructed, we can find the optimal path and the utility distribution for that strategy. In the background we use the JuMP Julia package and the Gurobi optimizer. First, we must define a JuMP model.
For this section you must have the Gurobi optimizer installed. If you are an academic, check the Gurobi academic license page and follow the instructions from there.
model = dp.Model()
We then extract the objective function from the diagram and use it in the JuMP model.
z = diagram.decision_variables(model)
x_s = diagram.path_compatibility_variables(model, z)
EV = diagram.expected_value(model, x_s)
model.objective(EV, "Max")
Then we set up the Gurobi optimizer and optimize the model.
model.setup_Gurobi_optimizer(
("IntFeasTol", 1e-9),
("LazyConstraints", 1)
)
model.optimize()
We can not extract the optimal decision strategy and the utility distribution.
Z = z.decision_strategy()
S_probabilities = diagram.state_probabilities(Z)
U_distribution = diagram.utility_distribution(Z)
To print the optimal decision strategy run
S_probabilities.print_decision_strategy()
For the utility distribution when following that strategy:
U_distribution.print_distribution()
And some statistical properties of the optimal utility distribution:
U_distribution.print_statistics()