Rolling dice

This section presents a number of examples based on rolling dice, using different representations and increasingly complex rules.

Annotated disjunctions: Dealing with multi-valued variables

We start with a variant of our earlier propositional coin example, but now using dice. It illustrates the use of annotated disjunctions, a probabilistic primitive that chooses exactly one of a number of alternatives (if their probabilities sum to 1.0, if the sum is less than 1.0, then with some probability none of the alternatives is picked). The first die is fair, the second has a higher chance of throwing a six. Furthermore, we define what it means to throw a six with both dice, or with at least one of them.

% annotated disjunctions 1/6::one1; 1/6::two1; 1/6::three1; 1/6::four1; 1/6::five1; 1/6::six1. 0.15::one2; 0.15::two2; 0.15::three2; 0.15::four2; 0.15::five2; 0.25::six2. % Rules: twoSix :- six1, six2. someSix :- six1. someSix :- six2. % Queries: query(six1). query(six2). query(twoSix). query(someSix).

Also try adding evidence(someSix). (short for evidence(someSix,true).) to the box above and observe how the probabilities change if we observe that there is at least one six.

Annotated disjunctions documentation

Negation as failure

Let’s now consider an example where we are interested in whether the dice show even or odd numbers. We change the propositional representation of the previous example so we can define these properties just once instead of for each die individually, and we’ll use Prolog’s negation as failure to define even in terms of odd.

% annotated disjunctions 1/6::one(1); 1/6::two(1); 1/6::three(1); 1/6::four(1); 1/6::five(1); 1/6::six(1). 0.15::one(2); 0.15::two(2); 0.15::three(2); 0.15::four(2); 0.15::five(2); 0.25::six(2). % Rules: odd(X) :- one(X). odd(X) :- three(X). odd(X) :- five(X). even(X) :- \+ odd(X). query(odd(1)). query(even(1)). query(odd(2)). query(even(2)).

Note that as in Prolog, negation has to be used with care, as it is only safe on ground goals. We illustrate this by posing non-ground queries (using an anonymous variable denoted by _ as argument) to the same program.

% annotated disjunctions 1/6::one(1); 1/6::two(1); 1/6::three(1); 1/6::four(1); 1/6::five(1); 1/6::six(1). 0.15::one(2); 0.15::two(2); 0.15::three(2); 0.15::four(2); 0.15::five(2); 0.25::six(2). % Rules: odd(X) :- one(X). odd(X) :- three(X). odd(X) :- five(X). even(X) :- \+ odd(X). query(odd(_)). query(even(_)).

For odd(_), this behaves as expected, i.e., it returns all ground instances of the query and their probabilities. The result reported for even(X1) is not meaningful. Obviously, this pitfall can easily be avoided here by writing explicit rules for even, which is left as an exercise for the reader.

Arithmetic expressions

We now consider yet another way to model dice, using fair ones only. This representation allows for convenient use of the results in arithmetic expressions, e.g., to add up the results from several dice. We query for the probabilities of the possible sums we can get from two dice given that the first number is even and the second odd.

1/6::dice(1,D); 1/6::dice(2,D); 1/6::dice(3,D); 1/6::dice(4,D); 1/6::dice(5,D); 1/6::dice(6,D) :- die(D). die(1). die(2). sum(S) :- dice(A,1), dice(B,2), S is A+B. odd(D) :- dice(1,D). odd(D) :- dice(3,D). odd(D) :- dice(5,D). even(D) :- \+ odd(D). query(sum(_)). evidence(even(1)). evidence(odd(2)).

It is easy to verify that the evidence restricts the possible outcomes to nine (equally likely) pairs, some of which result in the same sum. Bias the dice by changing the probability distribution or use different distributions for the two dice to see how this changes the result.


Let’s add another die, and ask for the sum of three rolls, given that the first number is smaller than the second, and the second smaller than the third.

1/6::dice(1,D); 1/6::dice(2,D); 1/6::dice(3,D); 1/6::dice(4,D); 1/6::dice(5,D); 1/6::dice(6,D) :- die(D). die(1). die(2). die(3). outcome(A,B,C) :- dice(A,1), dice(B,2), dice(C,3). increasing :- outcome(A,B,C), A<B, B<C. sum(S) :- outcome(A,B,C), S is A+B+C. query(sum(_)). evidence(increasing).

Add query(outcome(_,_,_)). to verify that indeed only increasing sequences have non-zero posterior probability.

Built-in predicate: between/3

Let’s consider an infinite number of dice, which we roll one after the other until we see a six for the first time. What is the probability of stopping after n dice? The first die is always rolled, those with higher numbers D are only rolled if the previous roll did not stop the process. We use the built-in predicate between/3 to restrict querying to n = 1,2,3,4,5.

1/6::dice(1,1); 1/6::dice(2,1); 1/6::dice(3,1); 1/6::dice(4,1); 1/6::dice(5,1); 1/6::dice(6,1). 1/6::dice(1,D); 1/6::dice(2,D); 1/6::dice(3,D); 1/6::dice(4,D); 1/6::dice(5,D); 1/6::dice(6,D) :- D > 1, P is D-1, continue(P). stop(N) :- dice(6,N). continue(N) :- dice(X,N), X < 6. query(stop(N)) :- between(1,5,N). % closed form probability computation %query(s(X)) :- between(1,5,X). %P::s(N) :- P is 1/6*(5/6)**(N-1).

Uncomment the last two lines to verify that ProbLog indeed computes the expected probabilities. The last line uses a flexible probability, i.e., a probability that is computed from the arguments of the atom it is attached to.

Recursion and Lists

The following example illustrates the use of a logic program with recursion and lists. We start by rolling the first die. Every roll determines the next die to roll, but we stop if we have used that die before. We query for the possible sequences of rolled dice. We use three-sided dice instead of the regular six-sided ones simply to restrict the number of possible outcomes (and thus inference time). We use the builtin between/3 to specify the identifiers of our dice, and regular Prolog definitions of member and reverse. The top level predicate roll calls the next predicate, which is responsible for recording which dice we have used and for determining when to stop.

1/3::dice(1,D); 1/3::dice(2,D); 1/3::dice(3,D) :- die(D). die(X) :- between(1,3,X). roll(L) :- next(1,[1],L). next(N,Seen,Rev) :- dice(Now,N), member(Now,Seen), reverse(Seen,[],Rev). next(N,Seen,List) :- dice(Now,N), \+ member(Now,Seen), next(Now,[Now|Seen],List). member(X,[X|_]). member(X,[_|Z]) :- member(X,Z). reverse([],L,L). reverse([H|T],A,L) :- reverse(T,[H|A],L). query(roll(_)).

Stochastic memoization

Much of the code of the previous example was for controlling the sequence of rolls. We’ll now change the rules to generate infinite sequences, and query for finite prefixes of these sequences (of length 5 in our case, thanks to the list pattern with five anonymous variables used in the query). Here’s a first model. We roll the first die for sure, as specified by the fact roll([1]), and if we already have seen a list of rolls, we take the first element F of the list, roll its die, and add the outcome H to the front of the extended sequence.

1/3::dice(1,D); 1/3::dice(2,D); 1/3::dice(3,D) :- die(D). die(X) :- between(1,3,X). roll([1]). roll([H,F|L]) :- roll([F|L]), dice(H,F). seen(L) :- reverse(L,[],R), roll(R). reverse([],L,L). reverse([H|T],A,L) :- reverse(T,[H|A],L). query(seen([_,_,_,_,_])).

The reason that most sequences have probability zero is that ProbLog uses stochastic memoization (or tabling), that is, if it encounters the same die a second time, it will not roll it again, but reuse the (memoized) result of the previous roll. Thus, if our first roll, which always uses die 1, produces a 1, then the sequence will only contain 1s, or if the sequence starts with 1 2 1, then it has to continue alternating between these two numbers. We can change this by adding an extra argument to the dice predicate that will be different on every call (sometimes called a trial identifier), as in the following program, which uses the sequence so far as extra argument. For fixed length, this results in a uniform distribution over all sequences of this length (change the length of the list pattern in the query to verify this).

1/3::dice(1,D,ID); 1/3::dice(2,D,ID); 1/3::dice(3,D,ID) :- die(D). die(X) :- between(1,3,X). roll([1]). roll([H,F|L]) :- roll([F|L]), dice(H,F,L). seen(L) :- reverse(L,[],R), roll(R). reverse([],L,L). reverse([H|T],A,L) :- reverse(T,[H|A],L). query(seen([_,_,_,_,_])).

Tabling (memoization) documentation