Theory Basic_Randomized_Algorithms

section ‹Basic Randomized Algorithms\label{sec:basic_randomized_algorithms}›

text ‹This section introduces a few randomized algorithms for well-known distributions. These both
serve as building blocks for more complex algorithms and as examples describing how to use the
framework.›

theory Basic_Randomized_Algorithms
  imports
    Randomized_Algorithm
    Probabilistic_While.Bernoulli
    Probabilistic_While.Geometric
    Permuted_Congruential_Generator
begin

text ‹A simple example: Here we define a randomized algorithm that can sample uniformly from
@{term "pmf_of_set {..<(2::nat)^n}"}. (The same problem for general ranges is discussed in
Section~\ref{sec:dice_roll}).›

fun binary_dice_roll :: "nat  nat random_alg"
  where
    "binary_dice_roll 0 = return_ra 0" |
    "binary_dice_roll (Suc n) =
      do { h  binary_dice_roll n;
           c  coin_ra;
           return_ra (of_bool c + 2 * h)
        }"

text ‹Because the algorithm terminates unconditionally it is easy to verify that
@{term "binary_dice_roll"} terminates almost surely:›

lemma binary_dice_roll_terminates: "terminates_almost_surely (binary_dice_roll n)"
  by (induction n) (auto intro:terminates_almost_surely_intros)

text ‹The corresponding PMF can be written as:›

fun binary_dice_roll_pmf :: "nat  nat pmf"
  where
    "binary_dice_roll_pmf 0 = return_pmf 0" |
    "binary_dice_roll_pmf (Suc n) =
      do { h  binary_dice_roll_pmf n;
           c  coin_pmf;
           return_pmf (of_bool c + 2 * h)
        }"

text ‹To verify that the distribution of the result of @{term "binary_dice_roll"} is
@{term "binary_dice_roll_pmf"} we can rely on the @{thm [source] pmf_of_ra_simps} simp rules
and the @{thm [source] "terminates_almost_surely_intros"} introduction rules:›

lemma "pmf_of_ra (binary_dice_roll n) = binary_dice_roll_pmf n"
  using binary_dice_roll_terminates
  by (induction n) (simp_all add:terminates_almost_surely_intros pmf_of_ra_simps)

text ‹Let us now consider an algorithm that does not terminate unconditionally but just almost
surely:›

partial_function (random_alg) binary_geometric :: "nat  nat random_alg"
  where
    "binary_geometric n =
      do { c  coin_ra;
           if c then (return_ra n) else binary_geometric (n+1)
        }"

text ‹This is necessary for running randomized algorithms defined with the
@{command "partial_function"} directive:›
declare binary_geometric.simps[code]

text ‹In this case, we need to map to an SPMF:›

partial_function (spmf) binary_geometric_spmf :: "nat  nat spmf"
  where
    "binary_geometric_spmf n =
      do { c  coin_spmf;
           if c then (return_spmf n) else binary_geometric_spmf (n+1)
        }"

text ‹We use the transfer rules for @{term "spmf_of_ra"} to show the correspondence:›

lemma binary_geometric_ra_correct:
  "spmf_of_ra (binary_geometric x) = binary_geometric_spmf x"
proof -
  include lifting_syntax
  have "((=) ===> rel_spmf_of_ra) binary_geometric_spmf binary_geometric"
    unfolding binary_geometric_def binary_geometric_spmf_def
    apply (rule fixp_ra_parametric[OF binary_geometric_spmf.mono binary_geometric.mono])
    by transfer_prover
  thus ?thesis
    unfolding rel_fun_def rel_spmf_of_ra_def by auto
qed

text ‹Bernoulli distribution: For this example we show correspondence with the already existing
definition of @{term "bernoulli"} SPMF.›

partial_function (random_alg) bernoulli_ra :: "real  bool random_alg" where
  "bernoulli_ra p = do {
     b  coin_ra;
     if b then return_ra (p  1 / 2)
     else if p < 1 / 2 then bernoulli_ra (2 * p)
     else bernoulli_ra (2 * p - 1)
   }"

declare bernoulli_ra.simps[code]

text ‹The following is a different technique to show equivalence of an SPMF with a randomized
algorithm. It only works if the SPMF has weight $1$. First we show that the SPMF is a lower
bound:›

lemma bernoulli_ra_correct_aux: "ord_spmf (=) (bernoulli x) (spmf_of_ra (bernoulli_ra x))"
proof (induction arbitrary:x rule:bernoulli.fixp_induct)
  case 1
  thus ?case by simp
next
  case 2
  thus ?case by simp
next
  case (3 p)
  thus ?case by (subst bernoulli_ra.simps)
      (auto intro:ord_spmf_bind_reflI simp:spmf_of_ra_simps)
qed

text ‹Then relying on the fact that the SPMF has weight one, we can derive equivalence:›

lemma bernoulli_ra_correct: "bernoulli x = spmf_of_ra (bernoulli_ra x)"
  using lossless_bernoulli weight_spmf_le_1 unfolding lossless_spmf_def
  by (intro eq_iff_ord_spmf[OF _ bernoulli_ra_correct_aux]) auto

text ‹Because @{term "bernoulli p"} is a lossless SPMF equivalent to
@{term "spmf_of_pmf (bernoulli_pmf p)"} it is also possible to express the above, without referring
to SPMFs:›

lemma
  "terminates_almost_surely (bernoulli_ra p)"
  "bernoulli_pmf p = pmf_of_ra (bernoulli_ra p)"
  unfolding terminates_almost_surely_def pmf_of_ra_def bernoulli_ra_correct[symmetric]
  by (simp_all add: bernoulli_eq_bernoulli_pmf pmf_of_spmf)

context
  includes lifting_syntax
begin

lemma bernoulli_ra_transfer [transfer_rule]:
  "((=) ===> rel_spmf_of_ra) bernoulli bernoulli_ra"
  unfolding rel_fun_def rel_spmf_of_ra_def bernoulli_ra_correct by simp

end

text ‹Using the randomized algorithm for the Bernoulli distribution, we can introduce one for the
general geometric distribution:›

partial_function (random_alg) geometric_ra :: "real  nat random_alg" where
  "geometric_ra p = do {
     b  bernoulli_ra p;
     if b then return_ra 0 else map_ra ((+) 1) (geometric_ra p)
  }"
declare geometric_ra.simps[code]

lemma geometric_ra_correct: "spmf_of_ra (geometric_ra x) = geometric_spmf x"
proof -
  include lifting_syntax
  have "((=) ===> rel_spmf_of_ra) geometric_spmf geometric_ra"
    unfolding geometric_ra_def geometric_spmf_def
    apply (rule fixp_ra_parametric[OF geometric_spmf.mono geometric_ra.mono])
    by transfer_prover
  thus ?thesis
    unfolding rel_fun_def rel_spmf_of_ra_def by auto
qed

text ‹Replication of a distribution›

fun replicate_ra :: "nat  'a random_alg  'a list random_alg"
  where
    "replicate_ra 0 f = return_ra []" |
    "replicate_ra (Suc n) f = do { xh  f; xt  replicate_ra n f; return_ra (xh#xt) }"

fun replicate_spmf :: "nat  'a spmf  'a list spmf"
  where
    "replicate_spmf 0 f = return_spmf []" |
    "replicate_spmf (Suc n) f = do { xh  f; xt  replicate_spmf n f; return_spmf (xh#xt) }"

lemma replicate_ra_correct: "spmf_of_ra (replicate_ra n f) = replicate_spmf n (spmf_of_ra f)"
  by (induction n) (auto simp :spmf_of_ra_simps)

lemma replicate_spmf_of_pmf: "replicate_spmf n (spmf_of_pmf f) = spmf_of_pmf (replicate_pmf n f)"
  by (induction n) (simp_all add:spmf_of_pmf_bind)

text ‹Binomial distribution›

definition binomial_ra :: "nat  real  nat random_alg"
  where "binomial_ra n p = map_ra (length  filter id) (replicate_ra n (bernoulli_ra p))"

lemma
  assumes "p  {0..1}"
  shows "spmf_of_ra (binomial_ra n p) = spmf_of_pmf (binomial_pmf n p)"
proof -
  have "spmf_of_ra (replicate_ra n (bernoulli_ra p))=spmf_of_pmf(replicate_pmf n (bernoulli_pmf p))"
    unfolding replicate_ra_correct bernoulli_ra_correct[symmetric] bernoulli_eq_bernoulli_pmf
    by (simp add:replicate_spmf_of_pmf)

  thus ?thesis
    unfolding binomial_pmf_altdef[OF assms] binomial_ra_def
    by (simp flip:map_spmf_of_pmf add:spmf_of_ra_map)
qed

text ‹Running randomized algorithms: Here we use the PRG introduced in
Section~\ref{sec:permuted_congruential_generator}.›

value "run_ra (binomial_ra 10 0.5) (random_coins 42)"

value "run_ra (replicate_ra 20 (bernoulli_ra 0.3)) (random_coins 42)"

end