;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Probability distribution application examples
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(ns
  #^{:author "Konrad Hinsen"
     :skip-wiki true
     :doc "Examples for finite probability distribution"}
  clojure.contrib.probabilities.examples-finite-distributions
  (:use [clojure.contrib.probabilities.finite-distributions
	 :only (uniform prob cond-prob join-with dist-m choose
	        normalize certainly cond-dist-m normalize-cond)])
  (:use [clojure.contrib.monads
	 :only (domonad with-monad m-seq m-chain m-lift)])
  (:require clojure.contrib.accumulators))

;; Simple examples using dice

; A single die is represented by a uniform distribution over the
; six possible outcomes.
(def die (uniform #{1 2 3 4 5 6}))

; The probability that the result is odd...
(prob odd? die)
; ... or greater than four.
(prob #(> % 4) die)

; The sum of two dice
(def two-dice (join-with + die die))
(prob #(> % 6) two-dice)

; The sum of two dice using a monad comprehension
(assert (= two-dice
	   (domonad dist-m
		    [d1 die
		     d2 die]
		    (+ d1 d2))))

; The two values separately, but as an ordered pair
(domonad dist-m
  [d1 die
   d2 die]
  (if (< d1 d2) (list d1 d2) (list d2 d1)))

; The conditional probability for two dice yielding X if X is odd:
(cond-prob odd? two-dice)

; A two-step experiment: throw a die, and then add 1 with probability 1/2
(domonad dist-m
  [d die
   x (choose (/ 1 2)  d
	     :else    (inc d))]
  x)

; The sum of n dice
(defn dice [n]
   (domonad dist-m
      [ds (m-seq (replicate n die))]
      (apply + ds)))

(assert (= two-dice (dice 2)))

(dice 3)


;; Construct an empirical distribution from counters

; Using an ordinary counter:
(def dist1
  (normalize
    (clojure.contrib.accumulators/add-items
      clojure.contrib.accumulators/empty-counter
      (for [_ (range 1000)] (rand-int 5)))))

; Or, more efficiently, using a counter that already keeps track of its total:
(def dist2
  (normalize
    (clojure.contrib.accumulators/add-items
      clojure.contrib.accumulators/empty-counter-with-total
      (for [_ (range 1000)] (rand-int 5)))))


;; The Monty Hall game
;; (see http://en.wikipedia.org/wiki/Monty_Hall_problem for a description)

; The set of doors. In the classical variant, there are three doors,
; but the code can also work with more than three doors.
(def doors #{:A :B :C})

; A simulation of the game, step by step:
(domonad dist-m
  [; The prize is hidden behind one of the doors.
   prize  (uniform doors)
   ; The player make his initial choice.
   choice (uniform doors)
   ; The host opens a door which is neither the prize door nor the
   ; one chosen by the player.
   opened (uniform (disj doors prize choice))
   ; If the player stays with his initial choice, the game ends and the
   ; following line should be commented out. It describes the switch from
   ; the initial choice to a door that is neither the opened one nor
   ; his original choice.
   choice (uniform (disj doors opened choice))
   ]
  ; If the chosen door has the prize behind it, the player wins.
  (if (= choice prize) :win :loose))


;; Tree growth simulation
;; Adapted from the code in:
;; Martin Erwig and Steve Kollmansberger,
;; "Probabilistic Functional Programming in Haskell",
;; Journal of Functional Programming, Vol. 16, No. 1, 21-34, 2006
;; http://web.engr.oregonstate.edu/~erwig/papers/abstracts.html#JFP06a

; A tree is represented by two attributes: its state (alive, hit, fallen),
; and its height (an integer). A new tree starts out alive and with zero height.
(def new-tree {:state :alive, :height 0})

; An evolution step in the simulation modifies alive trees only. They can
; either grow by one (90% probability), be hit by lightning and then stop
; growing (4% probability), or fall down (6% probability).
(defn evolve-1 [tree]
  (let [{s :state h :height} tree]
    (if (= s :alive)
      (choose 0.9   (assoc tree :height (inc (:height tree)))
	      0.04  (assoc tree :state :hit) 
	      :else {:state :fallen, :height 0})
      (certainly tree))))

; Multiple evolution steps can be chained together with m-chain,
; since each step's input is the output of the previous step.
(with-monad dist-m
  (defn evolve [n tree]
    ((m-chain (replicate n evolve-1)) tree)))

; Try it for zero, one, or two steps.
(evolve 0 new-tree)
(evolve 1 new-tree)
(evolve 2 new-tree)

; We can also get a distribution of the height only:
(with-monad dist-m
  ((m-lift 1 :height) (evolve 2 new-tree)))



;; Bayesian inference
;;
;; Suppose someone has three dice, one with six faces, one with eight, and
;; one with twelve. This person throws one die and gives us the number,
;; but doesn't tell us which die it was. What are the Bayesian probabilities
;; for each of the three dice, given the observation we have?

; A function that returns the distribution of a dice with n faces.
(defn die-n [n] (uniform (range 1 (inc n))))

; The three dice in the game with their distributions. With this map, we
; can easily calculate the probability for an observation under the
; condition that a particular die was used.
(def dice {:six     (die-n 6)
	   :eight   (die-n 8)
	   :twelve  (die-n 12)})

; The only prior knowledge is that one of the three dice is used, so we
; have no better than a uniform distribution to start with.
(def prior (uniform (keys dice)))

; Add a single observation to the information contained in the
; distribution. Adding an observation consists of
; 1) Draw a die from the prior distribution.
; 2) Draw an observation from the distribution of that die.
; 3) Eliminate (replace by nil) the trials that do not match the observation.
; 4) Normalize the distribution for the non-nil values.
(defn add-observation [prior observation]
  (normalize-cond
    (domonad cond-dist-m
      [die    prior
       number (get dice die)
       :when  (= number observation) ]
      die)))

; Add one observation.
(add-observation prior 1)

; Add three consecutive observations.
(-> prior (add-observation 1)
          (add-observation 3)
	  (add-observation 7))

; We can also add multiple observations in a single trial, but this
; is slower because more combinations have to be taken into account.
; With Bayesian inference, it is most efficient to eliminate choices
; as early as possible.
(defn add-observations [prior observations]
  (with-monad cond-dist-m
    (let [n-nums #(m-seq (replicate (count observations) (get dice %)))]
      (normalize-cond
        (domonad
          [die    prior
           nums   (n-nums die)
	   :when  (= nums observations)]
	  die)))))

(add-observations prior [1 3 7])
